From 1cd64eb83015011a94c1dabf3634035ae2b30146 Mon Sep 17 00:00:00 2001 From: "Jonah H. Harris" Date: Fri, 17 Jul 2026 15:58:59 -0400 Subject: [PATCH] Refactor dialect front ends behind a PlxSurface.parse_body vtable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- Makefile | 4 +- src/plx_dialect_cobol.c | 2218 ++++++++- src/plx_dialect_go.c | 1933 +++++++- src/plx_dialect_js.c | 5 +- src/plx_dialect_php.c | 5 +- src/plx_dialect_plsql.c | 415 +- src/plx_dialect_python.c | 550 ++- src/plx_dialect_ruby.c | 758 ++++ src/plx_dialect_ts.c | 10 +- src/plx_dialect_tsql.c | 1517 ++++++- src/plx_engine.h | 150 + src/plx_int.h | 12 + src/plx_parse_brace.c | 1102 +++++ src/plx_transpile.c | 9138 ++------------------------------------ 14 files changed, 8988 insertions(+), 8829 deletions(-) create mode 100644 src/plx_engine.h create mode 100644 src/plx_parse_brace.c diff --git a/Makefile b/Makefile index e7bd443..b121bff 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # plx - PGXS build MODULE_big = plx -OBJS = src/plx_core.o src/plx_transpile.o src/plx_strbuild.o src/plx_dialect_ruby.o src/plx_dialect_php.o src/plx_dialect_js.o src/plx_dialect_python.o src/plx_dialect_cobol.o src/plx_dialect_plsql.o src/plx_dialect_ts.o src/plx_dialect_tsql.o src/plx_dialect_go.o +OBJS = src/plx_core.o src/plx_transpile.o src/plx_strbuild.o src/plx_dialect_ruby.o src/plx_dialect_php.o src/plx_dialect_js.o src/plx_dialect_python.o src/plx_dialect_cobol.o src/plx_dialect_plsql.o src/plx_dialect_ts.o src/plx_dialect_tsql.o src/plx_dialect_go.o src/plx_parse_brace.o EXTENSION = plx DATA = plx--1.0.sql plx--1.1.sql plx--1.1.1.sql plx--1.2.sql plx--1.2.1.sql plx--1.2.2.sql plx--1.3.0.sql plx--1.3.1.sql plx--1.0--1.1.sql plx--1.1--1.1.1.sql plx--1.1.1--1.2.sql plx--1.2--1.2.1.sql plx--1.2.1--1.2.2.sql plx--1.2.2--1.3.0.sql plx--1.3.0--1.3.1.sql @@ -17,4 +17,4 @@ include $(PGXS) # PGXS does not track header dependencies; declare them so a header change # rebuilds every object (the shared enum/struct layout must stay consistent). -$(OBJS): src/plx.h src/plx_int.h +$(OBJS): src/plx.h src/plx_int.h src/plx_engine.h diff --git a/src/plx_dialect_cobol.c b/src/plx_dialect_cobol.c index 651ec62..5fac9db 100644 --- a/src/plx_dialect_cobol.c +++ b/src/plx_dialect_cobol.c @@ -3,22 +3,2231 @@ * * COBOL is verb-driven and free-format (COBOL 2023). Unlike the other dialects * it does not fit the shared byte lexer's expression model (hyphenated words, - * period sentence terminators, PICTURE clauses), so the COBOL front end in - * plx_transpile.c uses its own tokenizer and recursive-descent parser and emits - * plpgsql directly. This surface is mostly a marker (block_style COBOL); the - * keyword table is unused by the COBOL path but kept for consistency. + * 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. */ #include "postgres.h" #include "fmgr.h" +#include "lib/stringinfo.h" +#include "miscadmin.h" #include "utils/memutils.h" #include "plx.h" #include "plx_int.h" +#include "plx_engine.h" PG_FUNCTION_INFO_V1(plx_cob_validator); PG_FUNCTION_INFO_V1(plx_cob_inline_handler); +/* ======================================================================== */ +/* COBOL front end (plxcobol, ISO/IEC 1989:2023). */ +/* */ +/* COBOL is verb-driven and free-format. It does not fit the shared byte */ +/* lexer's expression model (hyphenated words, period sentence terminators, */ +/* PICTURE clauses), so this is a self-contained tokenizer + recursive- */ +/* descent parser that emits plpgsql directly into cx->out and declarations */ +/* into cx->locals. Compound statements use explicit COBOL 2023 scope */ +/* terminators (END-IF, END-PERFORM, END-EVALUATE). Data names are mapped */ +/* to plpgsql identifiers: lower-cased, hyphens become underscores. */ +/* ======================================================================== */ + +typedef enum +{ + CB_EOF, CB_WORD, CB_NUM, CB_STR, CB_PERIOD, CB_LP, CB_RP, CB_OP +} CbKind; + +typedef struct +{ + CbKind kind; + const char *s; + int len; + int line; + bool sp; /* whitespace preceded this token */ +} CbTok; + +typedef struct +{ + Ctx *cx; + CbTok *t; + int nt, pos; +} Cb; + +/* case-insensitive: is token tk exactly the reserved word w? */ +static bool +cob_ci(CbTok *tk, const char *w) +{ + int l = (int) strlen(w); + + return tk->kind == CB_WORD && tk->len == l && pg_strncasecmp(tk->s, w, l) == 0; +} + +/* map a COBOL data name to a plpgsql identifier: lower-case, '-' -> '_' */ +static char * +cob_map(const char *s, int len) +{ + char *r = palloc(len + 1); + int i; + + for (i = 0; i < len; i++) + { + char c = s[i]; + + if (c == '-') + c = '_'; + else if (c >= 'A' && c <= 'Z') + c = c - 'A' + 'a'; + r[i] = c; + } + r[len] = '\0'; + return r; +} + +static CbTok * +cob_cur(Cb *cb) +{ + return &cb->t[cb->pos]; +} + +pg_noreturn static void +cob_err(Cb *cb, const char *msg) +{ + plx_err(cb->cx, cb->t[cb->pos].line, "%s", msg); +} + +static void +cob_lex(Cb *cb, const char *body) +{ + const char *p = body, + *end = body + strlen(body); + int line = 1, + cap = 256, + n = 0, + paren = 0; + CbTok *t = palloc(sizeof(CbTok) * cap); + bool sp = true; + +#define CBPUSH(k, st, ln) do { \ + if (n >= cap) { cap *= 2; t = repalloc(t, sizeof(CbTok) * cap); } \ + t[n].kind = (k); t[n].s = (st); t[n].len = (ln); \ + t[n].line = line; t[n].sp = sp; n++; sp = false; \ + } while (0) + + while (p < end) + { + if (*p == '\n') + { + line++; + p++; + sp = true; + continue; + } + if (*p == ' ' || *p == '\t' || *p == '\r' || *p == ';') + { + p++; + sp = true; + continue; + } + if (*p == ',') + { + /* COBOL uses ',' as an optional separator between operands; strip it + * at statement level, but keep it inside parentheses so multi-argument + * SQL function calls (mod(a, b)) survive into the expression. */ + if (paren > 0) + { + CBPUSH(CB_OP, p, 1); + p++; + continue; + } + p++; + sp = true; + continue; + } + if (p[0] == '*' && p + 1 < end && p[1] == '>') /* *> line comment */ + { + while (p < end && *p != '\n') + p++; + continue; + } + if (p[0] == '>' && p + 1 < end && p[1] == '>') /* >> directive line */ + { + while (p < end && *p != '\n') + p++; + continue; + } + if (*p == '"' || *p == '\'') /* string literal */ + { + char q = *p; + const char *st = p; + bool closed = false; + + p++; + while (p < end) + { + if (*p == q) + { + if (p + 1 < end && p[1] == q) /* doubled quote */ + { + p += 2; + continue; + } + p++; + closed = true; + break; + } + if (*p == '\n') + line++; + p++; + } + if (!closed) + plx_err(cb->cx, line, "unterminated string literal"); + CBPUSH(CB_STR, st, (int) (p - st)); + continue; + } + if (*p >= '0' && *p <= '9') /* numeric literal / level */ + { + const char *st = p; + + while (p < end && *p >= '0' && *p <= '9') + p++; + if (p + 1 < end && *p == '.' && p[1] >= '0' && p[1] <= '9') + { + p++; + while (p < end && *p >= '0' && *p <= '9') + p++; + } + CBPUSH(CB_NUM, st, (int) (p - st)); + continue; + } + if ((*p >= 'A' && *p <= 'Z') || (*p >= 'a' && *p <= 'z') || *p == '_') + { /* COBOL word (may contain '-', '.') */ + const char *st = p; + + while (p < end) + { + char c = *p; + + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '_' || c == '-') + { + p++; + continue; + } + /* '.' inside a word (qualified name row.field), not a period */ + if (c == '.' && p + 1 < end && + (((p[1] >= 'A' && p[1] <= 'Z') || (p[1] >= 'a' && p[1] <= 'z') || + (p[1] >= '0' && p[1] <= '9') || p[1] == '_'))) + { + p++; + continue; + } + break; + } + while (p > st && p[-1] == '-') /* trailing '-' is not part */ + p--; + CBPUSH(CB_WORD, st, (int) (p - st)); + continue; + } + if (*p == '.') + { + CBPUSH(CB_PERIOD, p, 1); + p++; + continue; + } + if (*p == '(') + { + CBPUSH(CB_LP, p, 1); + p++; + paren++; + continue; + } + if (*p == ')') + { + CBPUSH(CB_RP, p, 1); + if (paren > 0) + paren--; + p++; + continue; + } + if (p[0] == '*' && p + 1 < end && p[1] == '*') /* ** exponent */ + { + CBPUSH(CB_OP, p, 2); + p += 2; + continue; + } + if (p + 1 < end && ((p[0] == '<' && p[1] == '>') || + (p[0] == '<' && p[1] == '=') || + (p[0] == '>' && p[1] == '='))) + { + CBPUSH(CB_OP, p, 2); + p += 2; + continue; + } + if (*p == '+' || *p == '-' || *p == '*' || *p == '/' || *p == '=' || + *p == '<' || *p == '>' || *p == ':' || *p == '%') + { + CBPUSH(CB_OP, p, 1); + p++; + continue; + } + p++; /* skip anything else */ + } + CBPUSH(CB_EOF, p, 0); + cb->t = t; + cb->nt = n; + cb->pos = 0; +#undef CBPUSH +} + +/* the COBOL statement verbs (a value expression never contains one) */ +static bool +cob_is_verb(CbTok *tk) +{ + static const char *v[] = { + "MOVE", "COMPUTE", "ADD", "SUBTRACT", "MULTIPLY", "DIVIDE", "IF", + "EVALUATE", "PERFORM", "DISPLAY", "RAISE", "ASSERT", "GOBACK", "RETURN", + "CONTINUE", "EXIT", "CALL", "EXECUTE", "COMMIT", "ROLLBACK", + "RETURN-NEXT", "RETURN-QUERY", "GET", "BEGIN-TRY", + "OPEN-CURSOR", "FETCH-CURSOR", "CLOSE-CURSOR", "MOVE-CURSOR", + "STRING-APPEND", NULL + }; + int i; + + if (tk->kind != CB_WORD) + return false; + for (i = 0; v[i]; i++) + if (cob_ci(tk, v[i])) + return true; + return false; +} + +/* words that close a block or clause */ +static bool +cob_is_close(CbTok *tk) +{ + static const char *c[] = { + "ELSE", "WHEN", "END-IF", "END-PERFORM", "END-EVALUATE", "END-EXEC", + "END-TRY", NULL + }; + int i; + + if (tk->kind != CB_WORD) + return false; + for (i = 0; c[i]; i++) + if (cob_ci(tk, c[i])) + return true; + return false; +} + +/* emit a COBOL string literal token as a SQL single-quoted literal */ +static void +cob_emit_str(CbTok *tk, StringInfo out) +{ + char q = tk->s[0]; + const char *s = tk->s + 1, + *e = tk->s + tk->len - 1; + + appendStringInfoChar(out, '\''); + while (s < e) + { + if (*s == q && s + 1 < e && s[1] == q) /* COBOL doubled quote */ + { + if (q == '\'') + appendStringInfoString(out, "''"); + else + appendStringInfoChar(out, '"'); + s += 2; + continue; + } + if (*s == '\'') /* escape for SQL */ + appendStringInfoString(out, "''"); + else + appendStringInfoChar(out, *s); + s++; + } + appendStringInfoChar(out, '\''); +} + +/* raw contents of a COBOL string literal (quotes stripped, doubles collapsed), + * for inlining as SQL text in a FOR ... IN loop or RETURN QUERY */ +static char * +cob_str_contents(CbTok *tk) +{ + StringInfoData s; + char q = tk->s[0]; + const char *p = tk->s + 1, + *e = tk->s + tk->len - 1; + + initStringInfo(&s); + while (p < e) + { + if (*p == q && p + 1 < e && p[1] == q) + { + appendStringInfoChar(&s, q); + p += 2; + continue; + } + appendStringInfoChar(&s, *p); + p++; + } + return s.data; +} + +/* a figurative constant -> SQL text, or NULL if not one */ +static const char * +cob_figurative(CbTok *tk) +{ + if (cob_ci(tk, "ZERO") || cob_ci(tk, "ZEROS") || cob_ci(tk, "ZEROES")) + return "0"; + if (cob_ci(tk, "SPACE") || cob_ci(tk, "SPACES")) + return "''"; + if (cob_ci(tk, "NULL") || cob_ci(tk, "NULLS")) + return "NULL"; + if (cob_ci(tk, "TRUE")) + return "true"; + if (cob_ci(tk, "FALSE")) + return "false"; + return NULL; +} + +/* append with a leading space when needed */ +static void +cob_sp(StringInfo out) +{ + if (out->len && out->data[out->len - 1] != ' ' && out->data[out->len - 1] != '(') + appendStringInfoChar(out, ' '); +} + +/* index of the ')' matching the '(' at index lp, within [.,limit), or -1 */ +static int +cob_match_lp(Cb *cb, int lp, int limit) +{ + int depth = 0, + i; + + for (i = lp; i < limit; i++) + { + if (cb->t[i].kind == CB_LP) + depth++; + else if (cb->t[i].kind == CB_RP) + { + depth--; + if (depth == 0) + return i; + } + else if (cb->t[i].kind == CB_EOF) + break; + } + return -1; +} + +/* is `name` a declared array local (its plpgsql type ends with "[]")? */ +static bool +cob_is_array(Cb *cb, const char *name) +{ + PlxLocal2 *l = plx_local_find(cb->cx, name, strlen(name)); + int tl; + + if (!l || !l->typ) + return false; + tl = (int) strlen(l->typ); + return tl >= 2 && l->typ[tl - 2] == '[' && l->typ[tl - 1] == ']'; +} + +/* + * Emit tokens [a,b) as a SQL expression / condition. Handles COBOL relational + * words (EQUAL TO, GREATER THAN [OR EQUAL TO], ...), NOT combinations, the IS + * noise word, figurative constants, ** -> ^, data-name mapping, and array + * subscripts (WS-ARR(I) -> ws_arr[i]). + */ +static void +cob_emit_range(Cb *cb, int a, int b, StringInfo out) +{ + CbTok *t = cb->t; + int i = a; + + while (i < b) + { + CbTok *tk = &t[i]; + const char *fig; + + if (tk->kind == CB_WORD) + { + if (cob_ci(tk, "IS")) + { + /* keep "IS" before NULL / NOT NULL; else it is noise */ + CbTok *nx = (i + 1 < b) ? &t[i + 1] : NULL; + + if (nx && (cob_ci(nx, "NULL") || cob_ci(nx, "NULLS") || + (cob_ci(nx, "NOT") && i + 2 < b && + (cob_ci(&t[i + 2], "NULL") || cob_ci(&t[i + 2], "NULLS"))))) + { + cob_sp(out); + appendStringInfoString(out, "IS"); + } + i++; + continue; + } + if (cob_ci(tk, "AND") || cob_ci(tk, "OR")) + { + cob_sp(out); + appendStringInfo(out, "%s", cob_ci(tk, "AND") ? "AND" : "OR"); + i++; + continue; + } + if (cob_ci(tk, "NOT")) + { + CbTok *nx = (i + 1 < b) ? &t[i + 1] : NULL; + + if (nx && (cob_ci(nx, "EQUAL") || cob_ci(nx, "EQUALS") || + (nx->kind == CB_OP && nx->len == 1 && nx->s[0] == '='))) + { + cob_sp(out); + appendStringInfoString(out, "<>"); + i += 2; + if (i < b && cob_ci(&t[i], "TO")) + i++; + continue; + } + if (nx && (cob_ci(nx, "GREATER") || + (nx->kind == CB_OP && nx->len == 1 && nx->s[0] == '>'))) + { + cob_sp(out); + appendStringInfoString(out, "<="); + i += 2; + if (i < b && cob_ci(&t[i], "THAN")) + i++; + continue; + } + if (nx && (cob_ci(nx, "LESS") || + (nx->kind == CB_OP && nx->len == 1 && nx->s[0] == '<'))) + { + cob_sp(out); + appendStringInfoString(out, ">="); + i += 2; + if (i < b && cob_ci(&t[i], "THAN")) + i++; + continue; + } + cob_sp(out); + appendStringInfoString(out, "NOT"); + i++; + continue; + } + if (cob_ci(tk, "EQUAL") || cob_ci(tk, "EQUALS")) + { + cob_sp(out); + appendStringInfoChar(out, '='); + i++; + if (i < b && cob_ci(&t[i], "TO")) + i++; + continue; + } + if (cob_ci(tk, "GREATER") || cob_ci(tk, "LESS")) + { + bool gt = cob_ci(tk, "GREATER"); + + i++; + if (i < b && cob_ci(&t[i], "THAN")) + i++; + if (i + 1 < b && cob_ci(&t[i], "OR") && + cob_ci(&t[i + 1], "EQUAL")) + { + i += 2; + if (i < b && cob_ci(&t[i], "TO")) + i++; + cob_sp(out); + appendStringInfoString(out, gt ? ">=" : "<="); + } + else + { + cob_sp(out); + appendStringInfoChar(out, gt ? '>' : '<'); + } + continue; + } + fig = cob_figurative(tk); + if (fig) + { + cob_sp(out); + appendStringInfoString(out, fig); + i++; + continue; + } + /* ordinary data name, or an array subscript WS-ARR(I) -> ws_arr[i] */ + cob_sp(out); + { + char *nm = cob_map(tk->s, tk->len); + + if (i + 1 < b && t[i + 1].kind == CB_LP && cob_is_array(cb, nm)) + { + int close = cob_match_lp(cb, i + 1, b); + + if (close > 0 && close < b) + { + appendStringInfoString(out, nm); + appendStringInfoChar(out, '['); + cob_emit_range(cb, i + 2, close, out); + appendStringInfoChar(out, ']'); + i = close + 1; + continue; + } + } + appendStringInfoString(out, nm); + } + i++; + continue; + } + if (tk->kind == CB_NUM) + { + cob_sp(out); + appendBinaryStringInfo(out, tk->s, tk->len); + i++; + continue; + } + if (tk->kind == CB_STR) + { + cob_sp(out); + cob_emit_str(tk, out); + i++; + continue; + } + if (tk->kind == CB_LP) + { + cob_sp(out); + appendStringInfoChar(out, '('); + i++; + continue; + } + if (tk->kind == CB_RP) + { + appendStringInfoChar(out, ')'); + i++; + continue; + } + if (tk->kind == CB_OP) + { + cob_sp(out); + if (tk->len == 2 && tk->s[0] == '*' && tk->s[1] == '*') + appendStringInfoChar(out, '^'); + else + appendBinaryStringInfo(out, tk->s, tk->len); + i++; + continue; + } + i++; + } +} + +/* true if the current token stops a value scan */ +static bool +cob_value_stop(Cb *cb, const char *const *stops, int nstops) +{ + CbTok *tk = cob_cur(cb); + int i; + + if (tk->kind == CB_EOF || tk->kind == CB_PERIOD) + return true; + if (cob_is_close(tk) || cob_is_verb(tk)) + return true; + for (i = 0; i < nstops; i++) + if (cob_ci(tk, stops[i])) + return true; + return false; +} + +/* scan and emit a value/condition until a stop token; returns false if empty */ +static bool +cob_value(Cb *cb, StringInfo out, const char *const *stops, int nstops) +{ + int start = cb->pos; + + while (!cob_value_stop(cb, stops, nstops)) + cb->pos++; + if (cb->pos == start) + return false; + cob_emit_range(cb, start, cb->pos, out); + return true; +} + +/* emit a single elementary operand (identifier / number / figurative), with + * optional leading sign; returns palloc'd SQL text, or NULL if none */ +static char * +cob_operand(Cb *cb) +{ + StringInfoData s; + CbTok *tk = cob_cur(cb); + bool neg = false; + + if (tk->kind == CB_OP && tk->len == 1 && (tk->s[0] == '-' || tk->s[0] == '+')) + { + neg = (tk->s[0] == '-'); + cb->pos++; + tk = cob_cur(cb); + } + initStringInfo(&s); + if (tk->kind == CB_NUM) + appendBinaryStringInfo(&s, tk->s, tk->len); + else if (tk->kind == CB_STR) + cob_emit_str(tk, &s); + else if (tk->kind == CB_WORD) + { + const char *fig = cob_figurative(tk); + + if (fig) + appendStringInfoString(&s, fig); + else + appendStringInfoString(&s, cob_map(tk->s, tk->len)); + } + else + return NULL; + cb->pos++; + if (neg) + { + StringInfoData r; + + initStringInfo(&r); + appendStringInfo(&r, "(- %s)", s.data); + return r.data; + } + return s.data; +} + +/* --------- PICTURE / declaration handling --------- */ + +static int +cob_pic_repeat(const char *p, int len, int *ip) +{ + int i = *ip, + n = 0; + + if (i < len && p[i] == '(') + { + i++; + while (i < len && p[i] >= '0' && p[i] <= '9') + { + n = n * 10 + (p[i++] - '0'); + if (n > 1000000) /* clamp: no real type is this wide */ + n = 1000000; + } + if (i < len && p[i] == ')') + i++; + *ip = i; + return n; + } + return 1; +} + +/* map a PICTURE string (+ optional USAGE word) to a plpgsql type */ +static char * +cob_type_from_pic(Cb *cb, const char *pic, int len, const char *usage, int ulen) +{ + int i = 0, + intdig = 0, + scale = 0, + strlen_ = 0; + bool isstr = false, + sawv = false; + char buf[64]; + + while (i < len) + { + char c = pic[i]; + + if (c >= 'a' && c <= 'z') + c = c - 'a' + 'A'; + if (c == 'S') + { + i++; + continue; + } + if (c == 'V' || c == '.') + { + sawv = true; + i++; + continue; + } + if (c == 'X' || c == 'A') + { + int r; + + isstr = true; + i++; + r = cob_pic_repeat(pic, len, &i); + strlen_ += r; + if (strlen_ > 1000000) /* clamp the running total, not just each group */ + strlen_ = 1000000; + continue; + } + if (c == '9') + { + int r; + + i++; + r = cob_pic_repeat(pic, len, &i); + if (sawv) + scale += r; + else + intdig += r; + if (intdig > 1000000) + intdig = 1000000; + if (scale > 1000000) + scale = 1000000; + continue; + } + i++; /* Z, comma, etc.: ignore */ + } + + if (usage) + { + if ((ulen == 6 && pg_strncasecmp(usage, "COMP-1", 6) == 0)) + return pstrdup("real"); + if ((ulen == 6 && pg_strncasecmp(usage, "COMP-2", 6) == 0)) + return pstrdup("double precision"); + } + + if (isstr) + { + if (strlen_ <= 0) + strlen_ = 1; + snprintf(buf, sizeof(buf), "varchar(%d)", strlen_); + return pstrdup(buf); + } + if (sawv || scale > 0) + { + int prec = intdig + scale; + + if (prec <= 0) + prec = scale + 1; + snprintf(buf, sizeof(buf), "numeric(%d,%d)", prec, scale); + return pstrdup(buf); + } + if (intdig == 0) + cob_err(cb, "PICTURE clause has no digits or characters"); + if (intdig <= 9) + return pstrdup("integer"); + if (intdig <= 18) + return pstrdup("bigint"); + snprintf(buf, sizeof(buf), "numeric(%d)", intdig); + return pstrdup(buf); +} + +/* parse one WORKING-STORAGE level entry (up to its terminating period) */ +static void +cob_decl(Cb *cb) +{ + CbTok *nm; + PlxLocal2 *l; + char *mapped; + const char *usage = NULL; + int ulen = 0; + bool is_array = false; + + /* level number */ + if (cob_cur(cb)->kind != CB_NUM) + cob_err(cb, "expected a level number in WORKING-STORAGE"); + cb->pos++; + /* data name */ + nm = cob_cur(cb); + if (nm->kind != CB_WORD) + cob_err(cb, "expected a data name after the level number"); + mapped = cob_map(nm->s, nm->len); + cb->pos++; + + if (plx_local_find(cb->cx, mapped, strlen(mapped))) + cob_err(cb, "duplicate data name in WORKING-STORAGE"); + l = plx_local_add(cb->cx, mapped, strlen(mapped)); + + while (cob_cur(cb)->kind != CB_PERIOD && cob_cur(cb)->kind != CB_EOF) + { + CbTok *tk = cob_cur(cb); + + if (cob_ci(tk, "PIC") || cob_ci(tk, "PICTURE")) + { + const char *rs, *re; + + cb->pos++; + if (cob_ci(cob_cur(cb), "IS")) + cb->pos++; + if (cob_cur(cb)->kind == CB_EOF || cob_cur(cb)->kind == CB_PERIOD) + cob_err(cb, "missing PICTURE string"); + rs = cob_cur(cb)->s; + re = cob_cur(cb)->s + cob_cur(cb)->len; + cb->pos++; + while (!cob_cur(cb)->sp && cob_cur(cb)->kind != CB_EOF && + cob_cur(cb)->kind != CB_PERIOD) + { + re = cob_cur(cb)->s + cob_cur(cb)->len; + cb->pos++; + } + l->typ = cob_type_from_pic(cb, rs, (int) (re - rs), usage, ulen); + } + else if (cob_ci(tk, "USAGE")) + { + cb->pos++; + if (cob_ci(cob_cur(cb), "IS")) + cb->pos++; + if (cob_cur(cb)->kind == CB_EOF || cob_cur(cb)->kind == CB_PERIOD) + cob_err(cb, "USAGE requires a value"); + usage = cob_cur(cb)->s; + ulen = cob_cur(cb)->len; + cb->pos++; + /* if PIC already set and usage is float, override */ + if (l->typ && ulen == 6 && pg_strncasecmp(usage, "COMP-1", 6) == 0) + l->typ = pstrdup("real"); + else if (l->typ && ulen == 6 && pg_strncasecmp(usage, "COMP-2", 6) == 0) + l->typ = pstrdup("double precision"); + } + else if (cob_ci(tk, "VALUE")) + { + StringInfoData v; + CbTok *vt; + const char *fig; + + cb->pos++; + if (cob_ci(cob_cur(cb), "IS")) + cb->pos++; + vt = cob_cur(cb); + initStringInfo(&v); + fig = (vt->kind == CB_WORD) ? cob_figurative(vt) : NULL; + if (fig) + { + if (strcmp(fig, "''") != 0 || true) + appendStringInfoString(&v, fig); + cb->pos++; + l->init = v.data; + } + else if (vt->kind == CB_NUM) + { + appendBinaryStringInfo(&v, vt->s, vt->len); + cb->pos++; + l->init = v.data; + } + else if (vt->kind == CB_STR) + { + cob_emit_str(vt, &v); + cb->pos++; + l->init = v.data; + } + else + cob_err(cb, "unsupported VALUE literal"); + } + else if (cob_ci(tk, "CONSTANT")) + { + StringInfoData v; + CbTok *vt; + + cb->pos++; + if (cob_ci(cob_cur(cb), "AS")) + cb->pos++; + vt = cob_cur(cb); + initStringInfo(&v); + cob_value(cb, &v, NULL, 0); + l->init = v.data; + l->is_const = true; + if (!l->typ) + { + if (vt->kind == CB_STR) + l->typ = pstrdup("text"); + else if (vt->kind == CB_NUM && + memchr(vt->s, '.', vt->len) != NULL) + l->typ = pstrdup("numeric"); + else + l->typ = pstrdup("integer"); + } + } + else if (cob_ci(tk, "TYPE")) + { + const char *rs, *re; + + cb->pos++; + rs = cob_cur(cb)->s; + re = rs; + while (cob_cur(cb)->kind != CB_PERIOD && cob_cur(cb)->kind != CB_EOF) + { + re = cob_cur(cb)->s + cob_cur(cb)->len; + cb->pos++; + } + if (re == rs) + cob_err(cb, "missing type after TYPE"); + { + char *ty = pnstrdup(rs, (int) (re - rs)); + + if (pg_strcasecmp(ty, "RECORD") == 0) + l->is_record = true; + else + l->typ = ty; + } + } + else if (cob_ci(tk, "OCCURS")) + { + /* OCCURS n [TIMES] -> the item is a PostgreSQL array of its element + * type (the fixed bound is not enforced) */ + cb->pos++; + if (cob_cur(cb)->kind != CB_NUM) + cob_err(cb, "OCCURS requires a count"); + cb->pos++; + if (cob_ci(cob_cur(cb), "TIMES")) + cb->pos++; + is_array = true; + } + else + cob_err(cb, "unexpected clause in a WORKING-STORAGE entry"); + } + if (cob_cur(cb)->kind == CB_PERIOD) + cb->pos++; + + if (is_array) + { + if (!l->typ) + cob_err(cb, "an OCCURS item needs a PIC or TYPE for its element type"); + l->typ = psprintf("%s[]", l->typ); + } + if (!l->typ && !l->is_record) + cob_err(cb, "data item needs a PIC, TYPE, or CONSTANT clause"); +} + +/* --------- statements --------- */ + +static void cob_block(Cb *cb, int ind); +static void cob_stmt(Cb *cb, int ind); + +/* expect and consume a specific closing word */ +static void +cob_expect(Cb *cb, const char *w) +{ + if (!cob_ci(cob_cur(cb), w)) + cob_err(cb, psprintf("expected %s", w)); + cb->pos++; +} + +/* read a receiving field (a data name, or an array element WS-ARR(I)) as a + * plpgsql lvalue string, consuming its tokens; NULL if the current token is not + * a data name */ +static char * +cob_target(Cb *cb) +{ + char *nm; + + if (cob_cur(cb)->kind != CB_WORD) + return NULL; + nm = cob_map(cob_cur(cb)->s, cob_cur(cb)->len); + cb->pos++; + if (cob_cur(cb)->kind == CB_LP && cob_is_array(cb, nm)) + { + int lp = cb->pos; + int close = cob_match_lp(cb, lp, cb->nt); + + if (close > lp) + { + StringInfoData s; + + initStringInfo(&s); + appendStringInfo(&s, "%s[", nm); + cob_emit_range(cb, lp + 1, close, &s); + appendStringInfoChar(&s, ']'); + cb->pos = close + 1; + return s.data; + } + } + return nm; +} + +static void +cob_move(Cb *cb, int ind) +{ + StringInfoData v; + static const char *stops[] = {"TO"}; + + cb->pos++; /* MOVE */ + initStringInfo(&v); + if (!cob_value(cb, &v, stops, 1)) + cob_err(cb, "MOVE requires a sending value"); + cob_expect(cb, "TO"); + if (cob_cur(cb)->kind != CB_WORD) + cob_err(cb, "MOVE requires at least one receiving field"); + while (cob_cur(cb)->kind == CB_WORD && !cob_is_verb(cob_cur(cb)) && + !cob_is_close(cob_cur(cb))) + { + char *tgt = cob_target(cb); + + plx_indent(&cb->cx->out, ind); + appendStringInfo(&cb->cx->out, "%s := %s;\n", tgt, v.data); + } +} + +static void +cob_compute(Cb *cb, int ind) +{ + char *tgts[16]; + int ntg = 0; + StringInfoData v; + int k; + + cb->pos++; /* COMPUTE */ + while (cob_cur(cb)->kind == CB_WORD) + { + if (ntg >= 16) + cob_err(cb, "too many COMPUTE receivers"); + tgts[ntg++] = cob_target(cb); + } + if (ntg == 0) + cob_err(cb, "COMPUTE requires a receiving field"); + if (!(cob_cur(cb)->kind == CB_OP && cob_cur(cb)->len == 1 && + cob_cur(cb)->s[0] == '=')) + cob_err(cb, "COMPUTE requires '='"); + cb->pos++; + initStringInfo(&v); + if (!cob_value(cb, &v, NULL, 0)) + cob_err(cb, "COMPUTE requires an expression"); + for (k = 0; k < ntg; k++) + { + plx_indent(&cb->cx->out, ind); + appendStringInfo(&cb->cx->out, "%s := (%s );\n", tgts[k], v.data); + } +} + +/* + * ADD / SUBTRACT / MULTIPLY / DIVIDE. + * ADD a b ... TO recv [GIVING g] recv|g := recv + (a+b+...) + * ADD a b ... GIVING g g := (a+b+...) + * SUBTRACT a b ... FROM recv [GIVING g] recv|g := recv - (a+b+...) + * MULTIPLY a BY recv [GIVING g] recv|g := recv * a + * DIVIDE a INTO recv [GIVING g] recv|g := recv / a + * DIVIDE a BY b GIVING g g := a / b + */ +static void +cob_arith(Cb *cb, int ind) +{ + CbTok *verb = cob_cur(cb); + char *recv, + *giv = NULL; + + cb->pos++; + + if (cob_ci(verb, "ADD") || cob_ci(verb, "SUBTRACT")) + { + bool is_add = cob_ci(verb, "ADD"); + StringInfoData sum; + int n = 0; + + /* operand list until TO / FROM / GIVING (ADD/SUBTRACT take many) */ + initStringInfo(&sum); + for (;;) + { + char *op; + + if (cob_ci(cob_cur(cb), "TO") || cob_ci(cob_cur(cb), "FROM") || + cob_ci(cob_cur(cb), "GIVING") || cob_value_stop(cb, NULL, 0)) + break; + op = cob_operand(cb); + if (!op) + break; + if (n++) + appendStringInfoString(&sum, " + "); + appendStringInfo(&sum, "%s", op); + } + if (n == 0) + cob_err(cb, "arithmetic verb requires an operand"); + + if (cob_ci(cob_cur(cb), is_add ? "TO" : "FROM")) + { + cb->pos++; + recv = cob_operand(cb); + if (!recv) + cob_err(cb, "arithmetic verb requires a receiving field"); + if (cob_ci(cob_cur(cb), "GIVING")) + { + cb->pos++; + giv = cob_operand(cb); + if (!giv) + cob_err(cb, "GIVING requires a receiving field"); + } + plx_indent(&cb->cx->out, ind); + appendStringInfo(&cb->cx->out, "%s := (%s %c (%s));\n", + giv ? giv : recv, recv, is_add ? '+' : '-', sum.data); + } + else if (is_add && cob_ci(cob_cur(cb), "GIVING")) + { + cb->pos++; + giv = cob_operand(cb); + if (!giv) + cob_err(cb, "GIVING requires a receiving field"); + plx_indent(&cb->cx->out, ind); + appendStringInfo(&cb->cx->out, "%s := (%s);\n", giv, sum.data); + } + else + cob_err(cb, is_add ? "ADD requires TO or GIVING" + : "SUBTRACT requires FROM"); + return; + } + + /* MULTIPLY / DIVIDE take a single source operand */ + { + char *src = cob_operand(cb); + const char *op; + + if (!src) + cob_err(cb, "arithmetic verb requires an operand"); + if (cob_ci(verb, "MULTIPLY")) + { + op = "*"; + cob_expect(cb, "BY"); + } + else /* DIVIDE */ + { + if (cob_ci(cob_cur(cb), "INTO")) + op = "/into"; + else if (cob_ci(cob_cur(cb), "BY")) + op = "/by"; + else + cob_err(cb, "DIVIDE requires INTO or BY"); + cb->pos++; + } + recv = cob_operand(cb); + if (!recv) + cob_err(cb, "arithmetic verb requires a receiving field"); + if (cob_ci(cob_cur(cb), "GIVING")) + { + cb->pos++; + giv = cob_operand(cb); + if (!giv) + cob_err(cb, "GIVING requires a receiving field"); + } + plx_indent(&cb->cx->out, ind); + if (strcmp(op, "*") == 0) + appendStringInfo(&cb->cx->out, "%s := (%s * %s);\n", + giv ? giv : recv, recv, src); + else if (strcmp(op, "/into") == 0) + appendStringInfo(&cb->cx->out, "%s := (%s / %s);\n", + giv ? giv : recv, recv, src); + else /* /by : DIVIDE a BY b GIVING g -> g := a / b */ + appendStringInfo(&cb->cx->out, "%s := (%s / %s);\n", + giv ? giv : recv, src, recv); + } +} + +static void +cob_if(Cb *cb, int ind) +{ + StringInfoData c; + static const char *stops[] = {"THEN"}; + + cb->pos++; /* IF */ + initStringInfo(&c); + if (!cob_value(cb, &c, stops, 1)) + cob_err(cb, "IF requires a condition"); + if (cob_ci(cob_cur(cb), "THEN")) + cb->pos++; + plx_indent(&cb->cx->out, ind); + appendStringInfo(&cb->cx->out, "IF %s THEN\n", c.data); + cob_block(cb, ind + 1); + if (cob_ci(cob_cur(cb), "ELSE")) + { + cb->pos++; + plx_indent(&cb->cx->out, ind); + appendStringInfoString(&cb->cx->out, "ELSE\n"); + cob_block(cb, ind + 1); + } + cob_expect(cb, "END-IF"); + plx_indent(&cb->cx->out, ind); + appendStringInfoString(&cb->cx->out, "END IF;\n"); +} + +static void +cob_evaluate(Cb *cb, int ind) +{ + bool searched; + + cb->pos++; /* EVALUATE */ + searched = cob_ci(cob_cur(cb), "TRUE"); + if (searched) + { + cb->pos++; + plx_indent(&cb->cx->out, ind); + appendStringInfoString(&cb->cx->out, "CASE\n"); + } + else + { + StringInfoData s; + static const char *stops[] = {"WHEN"}; + + initStringInfo(&s); + if (!cob_value(cb, &s, stops, 1)) + cob_err(cb, "EVALUATE requires a subject"); + plx_indent(&cb->cx->out, ind); + appendStringInfo(&cb->cx->out, "CASE %s\n", s.data); + } + + while (cob_ci(cob_cur(cb), "WHEN")) + { + if (cob_ci(&cb->t[cb->pos + 1], "OTHER")) + { + cb->pos += 2; + plx_indent(&cb->cx->out, ind + 1); + appendStringInfoString(&cb->cx->out, "ELSE\n"); + cob_block(cb, ind + 2); + break; + } + if (searched) + { + StringInfoData c; + + cb->pos++; + initStringInfo(&c); + if (!cob_value(cb, &c, NULL, 0)) + cob_err(cb, "WHEN requires a condition"); + plx_indent(&cb->cx->out, ind + 1); + appendStringInfo(&cb->cx->out, "WHEN %s THEN\n", c.data); + cob_block(cb, ind + 2); + } + else + { + StringInfoData vals; + bool first = true; + + initStringInfo(&vals); + /* stacked WHEN v1 WHEN v2 ... share the following statements */ + while (cob_ci(cob_cur(cb), "WHEN") && + !cob_ci(&cb->t[cb->pos + 1], "OTHER")) + { + StringInfoData one; + + cb->pos++; + initStringInfo(&one); + if (!cob_value(cb, &one, NULL, 0)) + cob_err(cb, "WHEN requires a value"); + if (!first) + appendStringInfoString(&vals, ", "); + appendStringInfo(&vals, "%s", one.data); + first = false; + } + plx_indent(&cb->cx->out, ind + 1); + appendStringInfo(&cb->cx->out, "WHEN %s THEN\n", vals.data); + cob_block(cb, ind + 2); + } + } + cob_expect(cb, "END-EVALUATE"); + plx_indent(&cb->cx->out, ind); + appendStringInfoString(&cb->cx->out, "END CASE;\n"); +} + +/* + * True if the UNTIL condition starting at `from` (the token after the control + * variable's comparison operator) is a *simple* "var op bound", i.e. the bound + * runs to the loop body with no logical connector or second comparison. A + * compound UNTIL (v > n AND done = 1) must use the general WHILE, not the + * integer-FOR fast path (which would otherwise fold the connector into the FOR + * bound and emit wrong output). + */ +static bool +cob_until_simple(Cb *cb, int from) +{ + int i, + depth = 0; + + for (i = from; i < cb->nt; i++) + { + CbTok *t = &cb->t[i]; + + if (t->kind == CB_LP) + depth++; + else if (t->kind == CB_RP) + depth--; + else if (t->kind == CB_PERIOD || t->kind == CB_EOF) + break; + else if (depth == 0 && (cob_is_verb(t) || cob_ci(t, "END-PERFORM"))) + break; /* the loop body / terminator begins here */ + else if (depth == 0 && + (cob_ci(t, "AND") || cob_ci(t, "OR") || cob_ci(t, "NOT") || + cob_ci(t, "EQUAL") || cob_ci(t, "EQUALS") || + cob_ci(t, "GREATER") || cob_ci(t, "LESS") || + (t->kind == CB_OP && t->len == 1 && + (t->s[0] == '<' || t->s[0] == '=' || t->s[0] == '>')))) + return false; /* a connector or second comparison */ + } + return true; +} + +static void +cob_perform(Cb *cb, int ind) +{ + cb->pos++; /* PERFORM */ + + if (cob_ci(cob_cur(cb), "UNTIL")) + { + StringInfoData c; + + cb->pos++; + initStringInfo(&c); + if (!cob_value(cb, &c, NULL, 0)) + cob_err(cb, "PERFORM UNTIL requires a condition"); + plx_indent(&cb->cx->out, ind); + appendStringInfo(&cb->cx->out, "WHILE NOT (%s ) LOOP\n", c.data); + cob_block(cb, ind + 1); + cob_expect(cb, "END-PERFORM"); + plx_indent(&cb->cx->out, ind); + appendStringInfoString(&cb->cx->out, "END LOOP;\n"); + } + else if (cob_ci(cob_cur(cb), "VARYING")) + { + char *var; + StringInfoData from, + by, + c; + static const char *s_by[] = {"BY"}; + static const char *s_until[] = {"UNTIL"}; + + cb->pos++; + if (cob_cur(cb)->kind != CB_WORD) + cob_err(cb, "PERFORM VARYING requires a control variable"); + var = cob_map(cob_cur(cb)->s, cob_cur(cb)->len); + cb->pos++; + cob_expect(cb, "FROM"); + initStringInfo(&from); + cob_value(cb, &from, s_by, 1); + cob_expect(cb, "BY"); + initStringInfo(&by); + cob_value(cb, &by, s_until, 1); + cob_expect(cb, "UNTIL"); + + /* + * Fast path: PERFORM VARYING v FROM a BY 1 UNTIL v > b (or >= b) is an + * integer FOR loop, which plpgsql runs faster than a WHILE with a manual + * increment. Requires BY 1 and a condition " > b" / " >= b". + */ + { + char *bytrim = by.data; + CbTok *c0 = cob_cur(cb); + /* c1 only valid when c0 is not EOF (EOF is the last token) */ + CbTok *c1 = (c0->kind != CB_EOF) ? &cb->t[cb->pos + 1] : c0; + bool gt = (c1->kind == CB_OP && c1->len == 1 && c1->s[0] == '>'); + bool ge = (c1->kind == CB_OP && c1->len == 2 && + c1->s[0] == '>' && c1->s[1] == '='); + + while (*bytrim == ' ') + bytrim++; + if (strcmp(bytrim, "1") == 0 && c0->kind == CB_WORD && (gt || ge) && + strcmp(cob_map(c0->s, c0->len), var) == 0 && + cob_until_simple(cb, cb->pos + 2)) + { + StringInfoData bound; + + cb->pos += 2; /* control variable and the operator */ + initStringInfo(&bound); + cob_value(cb, &bound, NULL, 0); + plx_indent(&cb->cx->out, ind); + if (gt) + appendStringInfo(&cb->cx->out, "FOR %s IN (%s )..(%s ) LOOP\n", + var, from.data, bound.data); + else + appendStringInfo(&cb->cx->out, "FOR %s IN (%s )..((%s ) - 1) LOOP\n", + var, from.data, bound.data); + cob_block(cb, ind + 1); + cob_expect(cb, "END-PERFORM"); + plx_indent(&cb->cx->out, ind); + appendStringInfoString(&cb->cx->out, "END LOOP;\n"); + return; + } + } + + /* general PERFORM VARYING -> WHILE with a manual step */ + initStringInfo(&c); + cob_value(cb, &c, NULL, 0); + plx_indent(&cb->cx->out, ind); + appendStringInfo(&cb->cx->out, "%s := (%s );\n", var, from.data); + plx_indent(&cb->cx->out, ind); + appendStringInfo(&cb->cx->out, "WHILE NOT (%s ) LOOP\n", c.data); + cob_block(cb, ind + 1); + plx_indent(&cb->cx->out, ind + 1); + appendStringInfo(&cb->cx->out, "%s := %s + (%s );\n", var, var, by.data); + cob_expect(cb, "END-PERFORM"); + plx_indent(&cb->cx->out, ind); + appendStringInfoString(&cb->cx->out, "END LOOP;\n"); + } + else if (cob_is_verb(cob_cur(cb))) + { + /* inline PERFORM ... END-PERFORM (no clause): unconditional loop */ + plx_indent(&cb->cx->out, ind); + appendStringInfoString(&cb->cx->out, "LOOP\n"); + cob_block(cb, ind + 1); + cob_expect(cb, "END-PERFORM"); + plx_indent(&cb->cx->out, ind); + appendStringInfoString(&cb->cx->out, "END LOOP;\n"); + } + else if (cob_cur(cb)->kind == CB_WORD && cob_ci(&cb->t[cb->pos + 1], "OVER")) + { + /* PERFORM OVER "" [USING ...] ... END-PERFORM (query loop) + * PERFORM OVER ARRAY ... END-PERFORM (array loop) */ + char *var = cob_map(cob_cur(cb)->s, cob_cur(cb)->len); + + cb->pos += 2; /* OVER */ + if (cob_ci(cob_cur(cb), "ARRAY")) + { + StringInfoData e; + + cb->pos++; + initStringInfo(&e); + if (!cob_value(cb, &e, NULL, 0)) + cob_err(cb, "PERFORM ... OVER ARRAY requires an array expression"); + plx_indent(&cb->cx->out, ind); + appendStringInfo(&cb->cx->out, "FOREACH %s IN ARRAY (%s ) LOOP\n", var, e.data); + } + else + { + PlxLocal2 *row; + static const char *stops[] = {"USING"}; + + row = plx_local_find(cb->cx, var, strlen(var)); + if (!row) + { + row = plx_local_add(cb->cx, var, strlen(var)); + row->is_record = true; + } + plx_indent(&cb->cx->out, ind); + if (cob_cur(cb)->kind == CB_STR && + !cob_ci(&cb->t[cb->pos + 1], "USING")) + { + /* literal SQL: inline it */ + char *sql = cob_str_contents(cob_cur(cb)); + + cb->pos++; + appendStringInfo(&cb->cx->out, "FOR %s IN %s LOOP\n", var, sql); + } + else + { + /* dynamic SQL / with binds: FOR ... IN EXECUTE [USING ...] */ + StringInfoData sql, + using; + int nusing = 0; + + initStringInfo(&sql); + cob_value(cb, &sql, stops, 1); + initStringInfo(&using); + if (cob_ci(cob_cur(cb), "USING")) + { + cb->pos++; + while (!cob_value_stop(cb, NULL, 0)) + { + char *op = cob_operand(cb); + + if (!op) + break; + if (nusing++) + appendStringInfoString(&using, ", "); + appendStringInfo(&using, "%s", op); + } + } + appendStringInfo(&cb->cx->out, "FOR %s IN EXECUTE %s", var, sql.data); + if (nusing) + appendStringInfo(&cb->cx->out, " USING %s", using.data); + appendStringInfoString(&cb->cx->out, " LOOP\n"); + } + } + cob_block(cb, ind + 1); + cob_expect(cb, "END-PERFORM"); + plx_indent(&cb->cx->out, ind); + appendStringInfoString(&cb->cx->out, "END LOOP;\n"); + } + else + { + /* PERFORM TIMES */ + StringInfoData n; + static const char *stops[] = {"TIMES"}; + int id = cb->cx->subq++; + + initStringInfo(&n); + if (!cob_value(cb, &n, stops, 1) || !cob_ci(cob_cur(cb), "TIMES")) + cob_err(cb, "PERFORM of a paragraph is not supported; use inline PERFORM ... END-PERFORM"); + cb->pos++; /* TIMES */ + plx_indent(&cb->cx->out, ind); + appendStringInfo(&cb->cx->out, "FOR __plx_cob_%d IN 1..(%s ) LOOP\n", id, n.data); + cob_block(cb, ind + 1); + cob_expect(cb, "END-PERFORM"); + plx_indent(&cb->cx->out, ind); + appendStringInfoString(&cb->cx->out, "END LOOP;\n"); + } +} + +static void +cob_display(Cb *cb, int ind) +{ + StringInfoData args; + int nargs = 0; + static const char *stops[] = {"UPON", "WITH"}; + + cb->pos++; /* DISPLAY */ + initStringInfo(&args); + while (!cob_value_stop(cb, stops, 2)) + { + char *op = cob_operand(cb); + + if (!op) + break; + if (nargs++) + appendStringInfoString(&args, ", "); + appendStringInfo(&args, "%s", op); + } + /* consume trailing UPON / WITH NO ADVANCING noise up to period */ + while (cob_cur(cb)->kind != CB_PERIOD && cob_cur(cb)->kind != CB_EOF && + !cob_is_verb(cob_cur(cb)) && !cob_is_close(cob_cur(cb))) + cb->pos++; + if (nargs == 0) + cob_err(cb, "DISPLAY requires at least one operand"); + plx_indent(&cb->cx->out, ind); + appendStringInfo(&cb->cx->out, "RAISE NOTICE '%%', concat(%s);\n", args.data); +} + +static void +cob_raise(Cb *cb, int ind) +{ + CbTok *lvl; + const char *level = "EXCEPTION"; + StringInfoData msg; + char *code = NULL; + static const char *stops[] = {"SQLSTATE"}; + + cb->pos++; /* RAISE */ + lvl = cob_cur(cb); + if (cob_ci(lvl, "EXCEPTION") || cob_ci(lvl, "ERROR")) + { + level = "EXCEPTION"; + cb->pos++; + } + else if (cob_ci(lvl, "NOTICE")) + { + level = "NOTICE"; + cb->pos++; + } + else if (cob_ci(lvl, "WARNING")) + { + level = "WARNING"; + cb->pos++; + } + else if (cob_ci(lvl, "INFO")) + { + level = "INFO"; + cb->pos++; + } + else if (cob_ci(lvl, "LOG")) + { + level = "LOG"; + cb->pos++; + } + else if (cob_ci(lvl, "DEBUG")) + { + level = "DEBUG"; + cb->pos++; + } + initStringInfo(&msg); + if (!cob_value(cb, &msg, stops, 1)) + cob_err(cb, "RAISE requires a message"); + if (cob_ci(cob_cur(cb), "SQLSTATE")) + { + cb->pos++; + if (cob_cur(cb)->kind != CB_STR) + cob_err(cb, "SQLSTATE requires a string code"); + { + StringInfoData cs; + + initStringInfo(&cs); + cob_emit_str(cob_cur(cb), &cs); + code = cs.data; + cb->pos++; + } + } + plx_indent(&cb->cx->out, ind); + if (code) + appendStringInfo(&cb->cx->out, "RAISE %s '%%',%s USING ERRCODE = %s;\n", + level, msg.data, code); + else + appendStringInfo(&cb->cx->out, "RAISE %s '%%',%s;\n", level, msg.data); +} + +static void +cob_assert(Cb *cb, int ind) +{ + StringInfoData c; + + cb->pos++; /* ASSERT */ + initStringInfo(&c); + if (!cob_value(cb, &c, NULL, 0)) + cob_err(cb, "ASSERT requires a condition"); + plx_indent(&cb->cx->out, ind); + appendStringInfo(&cb->cx->out, "ASSERT %s;\n", c.data); +} + +static void +cob_return(Cb *cb, int ind) +{ + bool goback = cob_ci(cob_cur(cb), "GOBACK"); + StringInfoData v; + + cb->pos++; /* GOBACK / RETURN */ + if (goback && cob_ci(cob_cur(cb), "RETURNING")) + cb->pos++; + initStringInfo(&v); + if (cob_value(cb, &v, NULL, 0)) + { + plx_indent(&cb->cx->out, ind); + appendStringInfo(&cb->cx->out, "RETURN %s;\n", v.data); + } + else + { + plx_indent(&cb->cx->out, ind); + appendStringInfoString(&cb->cx->out, "RETURN;\n"); + } +} + +static void +cob_exit(Cb *cb, int ind) +{ + cb->pos++; /* EXIT */ + if (!cob_ci(cob_cur(cb), "PERFORM")) + cob_err(cb, "only EXIT PERFORM [CYCLE] is supported"); + cb->pos++; + plx_indent(&cb->cx->out, ind); + if (cob_ci(cob_cur(cb), "CYCLE")) + { + cb->pos++; + appendStringInfoString(&cb->cx->out, "CONTINUE;\n"); + } + else + appendStringInfoString(&cb->cx->out, "EXIT;\n"); +} + +static void +cob_call(Cb *cb, int ind) +{ + char *name; + StringInfoData args; + int nargs = 0; + + cb->pos++; /* CALL */ + if (cob_cur(cb)->kind == CB_STR) + { + CbTok *tk = cob_cur(cb); + + name = pnstrdup(tk->s + 1, tk->len - 2); + cb->pos++; + } + else if (cob_cur(cb)->kind == CB_WORD) + { + name = cob_map(cob_cur(cb)->s, cob_cur(cb)->len); + cb->pos++; + } + else + cob_err(cb, "CALL requires a procedure name"); + initStringInfo(&args); + if (cob_ci(cob_cur(cb), "USING")) + { + cb->pos++; + while (!cob_value_stop(cb, NULL, 0)) + { + char *op = cob_operand(cb); + + if (!op) + break; + if (nargs++) + appendStringInfoString(&args, ", "); + appendStringInfo(&args, "%s", op); + } + } + plx_indent(&cb->cx->out, ind); + appendStringInfo(&cb->cx->out, "CALL %s(%s);\n", name, args.data); +} + +static void +cob_execute(Cb *cb, int ind) +{ + StringInfoData sql, + into, + using; + int nusing = 0; + static const char *stops[] = {"USING", "INTO"}; + + cb->pos++; /* EXECUTE */ + initStringInfo(&sql); + if (!cob_value(cb, &sql, stops, 2)) + cob_err(cb, "EXECUTE requires a command string"); + initStringInfo(&into); + initStringInfo(&using); + /* INTO and USING may appear in either order */ + for (;;) + { + if (cob_ci(cob_cur(cb), "INTO")) + { + int nt = 0; + + cb->pos++; + while (cob_cur(cb)->kind == CB_WORD && !cob_is_verb(cob_cur(cb)) && + !cob_is_close(cob_cur(cb)) && !cob_ci(cob_cur(cb), "USING")) + { + if (nt++) + appendStringInfoString(&into, ", "); + appendStringInfoString(&into, cob_map(cob_cur(cb)->s, cob_cur(cb)->len)); + cb->pos++; + } + } + else if (cob_ci(cob_cur(cb), "USING")) + { + static const char *ustops[] = {"INTO"}; + + cb->pos++; + while (!cob_value_stop(cb, ustops, 1)) + { + char *op = cob_operand(cb); + + if (!op) + break; + if (nusing++) + appendStringInfoString(&using, ", "); + appendStringInfo(&using, "%s", op); + } + } + else + break; + } + plx_indent(&cb->cx->out, ind); + appendStringInfo(&cb->cx->out, "EXECUTE %s", sql.data); + if (into.len) + appendStringInfo(&cb->cx->out, " INTO %s", into.data); + if (nusing) + appendStringInfo(&cb->cx->out, " USING %s", using.data); + appendStringInfoString(&cb->cx->out, ";\n"); +} + +static void +cob_return_next(Cb *cb, int ind) +{ + StringInfoData v; + + cb->pos++; /* RETURN-NEXT */ + initStringInfo(&v); + if (!cob_value(cb, &v, NULL, 0)) + cob_err(cb, "RETURN-NEXT requires a value"); + plx_indent(&cb->cx->out, ind); + appendStringInfo(&cb->cx->out, "RETURN NEXT %s;\n", v.data); +} + +static void +cob_return_query(Cb *cb, int ind) +{ + static const char *stops[] = {"USING"}; + + cb->pos++; /* RETURN-QUERY */ + plx_indent(&cb->cx->out, ind); + if (cob_cur(cb)->kind == CB_STR && !cob_ci(&cb->t[cb->pos + 1], "USING")) + { + char *sql = cob_str_contents(cob_cur(cb)); + + cb->pos++; + appendStringInfo(&cb->cx->out, "RETURN QUERY %s;\n", sql); + } + else + { + StringInfoData sql, + using; + int nusing = 0; + + initStringInfo(&sql); + if (!cob_value(cb, &sql, stops, 1)) + cob_err(cb, "RETURN-QUERY requires a command string"); + initStringInfo(&using); + if (cob_ci(cob_cur(cb), "USING")) + { + cb->pos++; + while (!cob_value_stop(cb, NULL, 0)) + { + char *op = cob_operand(cb); + + if (!op) + break; + if (nusing++) + appendStringInfoString(&using, ", "); + appendStringInfo(&using, "%s", op); + } + } + appendStringInfo(&cb->cx->out, "RETURN QUERY EXECUTE %s", sql.data); + if (nusing) + appendStringInfo(&cb->cx->out, " USING %s", using.data); + appendStringInfoString(&cb->cx->out, ";\n"); + } +} + +static void +cob_get(Cb *cb, int ind) +{ + char *var; + const char *field; + bool stacked = false; + CbTok *f; + + cb->pos++; /* GET */ + f = cob_cur(cb); + if (cob_ci(f, "ROW-COUNT")) + field = "ROW_COUNT"; + else if (cob_ci(f, "MESSAGE")) + { + field = "MESSAGE_TEXT"; + stacked = true; + } + else if (cob_ci(f, "DETAIL")) + { + field = "PG_EXCEPTION_DETAIL"; + stacked = true; + } + else if (cob_ci(f, "HINT")) + { + field = "PG_EXCEPTION_HINT"; + stacked = true; + } + else if (cob_ci(f, "SQLSTATE")) + { + field = "RETURNED_SQLSTATE"; + stacked = true; + } + else if (cob_ci(f, "CONTEXT")) + { + field = "PG_EXCEPTION_CONTEXT"; + stacked = true; + } + else + cob_err(cb, "GET expects ROW-COUNT, MESSAGE, DETAIL, HINT, SQLSTATE, or CONTEXT"); + cb->pos++; + if (cob_ci(cob_cur(cb), "INTO")) + cb->pos++; + if (cob_cur(cb)->kind != CB_WORD) + cob_err(cb, "GET requires a receiving field"); + var = cob_map(cob_cur(cb)->s, cob_cur(cb)->len); + cb->pos++; + plx_indent(&cb->cx->out, ind); + appendStringInfo(&cb->cx->out, "GET %sDIAGNOSTICS %s = %s;\n", + stacked ? "STACKED " : "", var, field); +} + +/* BEGIN-TRY ... [WHEN |OTHER ...] END-TRY -> BEGIN..EXCEPTION..END */ +static void +cob_try(Cb *cb, int ind) +{ + cb->pos++; /* BEGIN-TRY */ + plx_indent(&cb->cx->out, ind); + appendStringInfoString(&cb->cx->out, "BEGIN\n"); + cob_block(cb, ind + 1); + if (cob_ci(cob_cur(cb), "WHEN")) + { + plx_indent(&cb->cx->out, ind); + appendStringInfoString(&cb->cx->out, "EXCEPTION\n"); + while (cob_ci(cob_cur(cb), "WHEN")) + { + char *cond; + + cb->pos++; + if (cob_ci(cob_cur(cb), "OTHER") || cob_ci(cob_cur(cb), "OTHERS")) + { + cond = "OTHERS"; + cb->pos++; + } + else if (cob_cur(cb)->kind == CB_WORD) + { + cond = cob_map(cob_cur(cb)->s, cob_cur(cb)->len); + cb->pos++; + } + else + cob_err(cb, "WHEN requires an exception condition name or OTHER"); + plx_indent(&cb->cx->out, ind + 1); + appendStringInfo(&cb->cx->out, "WHEN %s THEN\n", cond); + cob_block(cb, ind + 2); + } + } + cob_expect(cb, "END-TRY"); + plx_indent(&cb->cx->out, ind); + appendStringInfoString(&cb->cx->out, "END;\n"); +} + +/* declare a name as a local of a given kind if it is not a parameter */ +static void +cob_declare_kind(Cb *cb, const char *name, const char *typ, bool is_record) +{ + PlxLocal2 *l; + + if (plx_is_param(cb->cx, name, strlen(name))) + return; + l = plx_local_find(cb->cx, name, strlen(name)); + if (!l) + l = plx_local_add(cb->cx, name, strlen(name)); + if (is_record) + l->is_record = true; + else if (!l->typ) + l->typ = pstrdup(typ); +} + +static void +cob_open_cursor(Cb *cb, int ind) +{ + char *c; + + cb->pos++; /* OPEN-CURSOR */ + if (cob_cur(cb)->kind != CB_WORD) + cob_err(cb, "OPEN-CURSOR requires a cursor name"); + c = cob_map(cob_cur(cb)->s, cob_cur(cb)->len); + cb->pos++; + cob_declare_kind(cb, c, "refcursor", false); + cob_expect(cb, "FOR"); + plx_indent(&cb->cx->out, ind); + if (cob_cur(cb)->kind == CB_STR && !cob_ci(&cb->t[cb->pos + 1], "USING")) + { + char *sql = cob_str_contents(cob_cur(cb)); + + cb->pos++; + appendStringInfo(&cb->cx->out, "OPEN %s FOR %s;\n", c, sql); + } + else + { + StringInfoData sql, + using; + int nusing = 0; + static const char *stops[] = {"USING"}; + + initStringInfo(&sql); + cob_value(cb, &sql, stops, 1); + initStringInfo(&using); + if (cob_ci(cob_cur(cb), "USING")) + { + cb->pos++; + while (!cob_value_stop(cb, NULL, 0)) + { + char *op = cob_operand(cb); + + if (!op) + break; + if (nusing++) + appendStringInfoString(&using, ", "); + appendStringInfo(&using, "%s", op); + } + } + appendStringInfo(&cb->cx->out, "OPEN %s FOR EXECUTE %s", c, sql.data); + if (nusing) + appendStringInfo(&cb->cx->out, " USING %s", using.data); + appendStringInfoString(&cb->cx->out, ";\n"); + } +} + +static void +cob_fetch_cursor(Cb *cb, int ind) +{ + char *c, + *row; + + cb->pos++; /* FETCH-CURSOR */ + if (cob_cur(cb)->kind != CB_WORD) + cob_err(cb, "FETCH-CURSOR requires a cursor name"); + c = cob_map(cob_cur(cb)->s, cob_cur(cb)->len); + cb->pos++; + cob_expect(cb, "INTO"); + if (cob_cur(cb)->kind != CB_WORD) + cob_err(cb, "FETCH-CURSOR requires a receiving record"); + row = cob_map(cob_cur(cb)->s, cob_cur(cb)->len); + cb->pos++; + cob_declare_kind(cb, row, NULL, true); + plx_indent(&cb->cx->out, ind); + appendStringInfo(&cb->cx->out, "FETCH FROM %s INTO %s;\n", c, row); +} + +static void +cob_close_cursor(Cb *cb, int ind) +{ + char *c; + + cb->pos++; /* CLOSE-CURSOR */ + if (cob_cur(cb)->kind != CB_WORD) + cob_err(cb, "CLOSE-CURSOR requires a cursor name"); + c = cob_map(cob_cur(cb)->s, cob_cur(cb)->len); + cb->pos++; + plx_indent(&cb->cx->out, ind); + appendStringInfo(&cb->cx->out, "CLOSE %s;\n", c); +} + +static void +cob_move_cursor(Cb *cb, int ind) +{ + char *c; + char *n = NULL; + + cb->pos++; /* MOVE-CURSOR */ + if (cob_cur(cb)->kind != CB_WORD) + cob_err(cb, "MOVE-CURSOR requires a cursor name"); + c = cob_map(cob_cur(cb)->s, cob_cur(cb)->len); + cb->pos++; + if (!cob_value_stop(cb, NULL, 0)) + n = cob_operand(cb); + plx_indent(&cb->cx->out, ind); + appendStringInfo(&cb->cx->out, "MOVE FORWARD %s FROM %s;\n", n ? n : "1", c); +} + +/* STRING-APPEND TO : lower to the plx_strbuild string builder + * (amortized-O(1) append), the COBOL counterpart of the other dialects' + * append operators. */ +static void +cob_string_append(Cb *cb, int ind) +{ + StringInfoData v; + char *tgt; + PlxLocal2 *l; + static const char *stops[] = {"TO"}; + + cb->pos++; /* STRING-APPEND */ + initStringInfo(&v); + if (!cob_value(cb, &v, stops, 1)) + cob_err(cb, "STRING-APPEND requires a value"); + cob_expect(cb, "TO"); + if (cob_cur(cb)->kind != CB_WORD) + cob_err(cb, "STRING-APPEND requires a receiving field"); + tgt = cob_map(cob_cur(cb)->s, cob_cur(cb)->len); + cb->pos++; + l = plx_local_find(cb->cx, tgt, strlen(tgt)); + if (l && !l->is_record) + l->typ = pstrdup("plx_strbuild"); /* the accumulator becomes a builder */ + plx_indent(&cb->cx->out, ind); + appendStringInfo(&cb->cx->out, "%s := plx_sb_append(%s, (%s )::text);\n", + tgt, tgt, v.data); +} + +static void +cob_stmt(Cb *cb, int ind) +{ + CbTok *tk = cob_cur(cb); + + if (cob_ci(tk, "MOVE")) + cob_move(cb, ind); + else if (cob_ci(tk, "STRING-APPEND")) + cob_string_append(cb, ind); + else if (cob_ci(tk, "RETURN-NEXT")) + cob_return_next(cb, ind); + else if (cob_ci(tk, "RETURN-QUERY")) + cob_return_query(cb, ind); + else if (cob_ci(tk, "GET")) + cob_get(cb, ind); + else if (cob_ci(tk, "BEGIN-TRY")) + cob_try(cb, ind); + else if (cob_ci(tk, "OPEN-CURSOR")) + cob_open_cursor(cb, ind); + else if (cob_ci(tk, "FETCH-CURSOR")) + cob_fetch_cursor(cb, ind); + else if (cob_ci(tk, "CLOSE-CURSOR")) + cob_close_cursor(cb, ind); + else if (cob_ci(tk, "MOVE-CURSOR")) + cob_move_cursor(cb, ind); + else if (cob_ci(tk, "COMPUTE")) + cob_compute(cb, ind); + else if (cob_ci(tk, "ADD") || cob_ci(tk, "SUBTRACT") || + cob_ci(tk, "MULTIPLY") || cob_ci(tk, "DIVIDE")) + cob_arith(cb, ind); + else if (cob_ci(tk, "IF")) + cob_if(cb, ind); + else if (cob_ci(tk, "EVALUATE")) + cob_evaluate(cb, ind); + else if (cob_ci(tk, "PERFORM")) + cob_perform(cb, ind); + else if (cob_ci(tk, "DISPLAY")) + cob_display(cb, ind); + else if (cob_ci(tk, "RAISE")) + cob_raise(cb, ind); + else if (cob_ci(tk, "ASSERT")) + cob_assert(cb, ind); + else if (cob_ci(tk, "GOBACK") || cob_ci(tk, "RETURN")) + cob_return(cb, ind); + else if (cob_ci(tk, "CONTINUE")) + { + cb->pos++; + plx_indent(&cb->cx->out, ind); + appendStringInfoString(&cb->cx->out, "NULL;\n"); + } + else if (cob_ci(tk, "EXIT")) + cob_exit(cb, ind); + else if (cob_ci(tk, "CALL")) + cob_call(cb, ind); + else if (cob_ci(tk, "EXECUTE")) + cob_execute(cb, ind); + else if (cob_ci(tk, "COMMIT")) + { + cb->pos++; + plx_indent(&cb->cx->out, ind); + appendStringInfoString(&cb->cx->out, "COMMIT;\n"); + } + else if (cob_ci(tk, "ROLLBACK")) + { + cb->pos++; + plx_indent(&cb->cx->out, ind); + appendStringInfoString(&cb->cx->out, "ROLLBACK;\n"); + } + else + cob_err(cb, psprintf("unexpected COBOL statement \"%.*s\"", tk->len, tk->s)); +} + +/* parse a statement list until a close word / EOF; periods separate sentences */ +static void +cob_block(Cb *cb, int ind) +{ + if (++cb->cx->depth > PLX_MAX_DEPTH) + cob_err(cb, "statements nested too deeply"); + for (;;) + { + while (cob_cur(cb)->kind == CB_PERIOD) + cb->pos++; + if (cob_cur(cb)->kind == CB_EOF || cob_is_close(cob_cur(cb))) + break; + cob_stmt(cb, ind); + } + cb->cx->depth--; +} + +void +plx_cobol_parse_body(Ctx *cx) +{ + Cb cb; + + memset(&cb, 0, sizeof(cb)); + cb.cx = cx; + cob_lex(&cb, cx->body); + + while (cob_cur(&cb)->kind == CB_PERIOD) + cb.pos++; + + /* optional DATA DIVISION. */ + if (cob_ci(cob_cur(&cb), "DATA") && cob_ci(&cb.t[cb.pos + 1], "DIVISION")) + { + cb.pos += 2; + if (cob_cur(&cb)->kind == CB_PERIOD) + cb.pos++; + } + /* optional WORKING-STORAGE SECTION. + level entries */ + if (cob_ci(cob_cur(&cb), "WORKING-STORAGE") && + cob_ci(&cb.t[cb.pos + 1], "SECTION")) + { + cb.pos += 2; + if (cob_cur(&cb)->kind == CB_PERIOD) + cb.pos++; + while (cob_cur(&cb)->kind == CB_NUM) + cob_decl(&cb); + } + /* optional PROCEDURE DIVISION [USING ...]. */ + if (cob_ci(cob_cur(&cb), "PROCEDURE") && cob_ci(&cb.t[cb.pos + 1], "DIVISION")) + { + cb.pos += 2; + while (cob_cur(&cb)->kind != CB_PERIOD && cob_cur(&cb)->kind != CB_EOF) + cb.pos++; + if (cob_cur(&cb)->kind == CB_PERIOD) + cb.pos++; + } + + cob_block(&cb, 1); + if (cob_cur(&cb)->kind != CB_EOF) + cob_err(&cb, "unexpected text after the procedure body"); +} + static const PlxSurface cob_surface = { .lanname = "plxcobol", .block_style = PLX_BLK_COBOL, @@ -39,6 +2248,7 @@ static const PlxSurface cob_surface = { .excs = NULL, .nexcs = 0, .flags = PLX_TRUSTED, + .parse_body = plx_cobol_parse_body, }; static char * diff --git a/src/plx_dialect_go.c b/src/plx_dialect_go.c index ede1b1a..6e79226 100644 --- a/src/plx_dialect_go.c +++ b/src/plx_dialect_go.c @@ -5,20 +5,1948 @@ * ends (ASI), uses parenless if/for headers, `:=` short declarations with type * inference, `for ... range`, and a switch without fall-through. Those differ * enough from the shared brace parser that plxgo has its own tokenizer and - * recursive-descent front end in plx_transpile.c (block_style GO); this surface - * is a marker. The shared keyword table is unused by the Go path. + * recursive-descent front end (below, wired via PlxSurface.parse_body); the + * dialect-neutral engine (lexer, expression rewriter, leaf emitter, assemble) + * lives in plx_transpile.c. The shared keyword table is unused by the Go path. */ #include "postgres.h" #include "fmgr.h" +#include "lib/stringinfo.h" +#include "miscadmin.h" #include "utils/memutils.h" #include "plx.h" #include "plx_int.h" +#include "plx_engine.h" PG_FUNCTION_INFO_V1(plx_go_validator); PG_FUNCTION_INFO_V1(plx_go_inline_handler); +/* ======================================================================== */ +/* Go front end (plxgo). */ +/* */ +/* Go is a C-family braced language but differs from plpgsql enough to need */ +/* its own tokenizer and recursive-descent parser (own lexer with automatic */ +/* semicolon insertion). It emits plpgsql directly, populating cx->locals + */ +/* cx->out for the shared assemble path: */ +/* - var x T [= e] / x := e (type inferred) / a, b := x, y -> DECLARE hoist*/ +/* - =, +=, ..., ++, -- assignment */ +/* - if cond { } [else if] [else] -> IF cond THEN ... END IF; */ +/* - for {} / for cond {} / for i:=a; c; p {} / for _,v := range e {} */ +/* - switch (tag and tagless) -> IF/ELSIF/ELSE */ +/* - return, break, continue, panic(); fmt.Println -> RAISE NOTICE */ +/* - emit()/execute()/range query() SQL bridge; a stdlib + type library. */ +/* ======================================================================== */ + +typedef enum +{ + GO_EOF, GO_WORD, GO_NUM, GO_STR, GO_OP, GO_LP, GO_RP, + GO_LBRACE, GO_RBRACE, GO_LBRACK, GO_RBRACK, GO_COMMA, GO_SEMI, GO_DOT +} GoKind; + +typedef struct +{ + GoKind kind; + const char *s; + int len; + int line; +} GoTok; + +typedef struct +{ + Ctx *cx; + GoTok *t; + int nt, pos; +} Go; + +static void go_stmt(Go *g, int ind); +static void go_block(Go *g, int ind); +static void go_emit_range(Go *g, int a, int b, StringInfo out); + +static GoTok * +go_cur(Go *g) +{ + return &g->t[g->pos]; +} + +static bool +go_ci(GoTok *tk, const char *w) +{ + int l = (int) strlen(w); + + /* Go keywords/identifiers are case-sensitive */ + return tk->kind == GO_WORD && tk->len == l && strncmp(tk->s, w, l) == 0; +} + +pg_noreturn static void +go_err(Go *g, const char *msg) +{ + plx_err(g->cx, g->t[g->pos].line, "%s", msg); +} + +static char * +go_ident(const char *s, int len) +{ + return pnstrdup(s, len); +} + +static void +go_sp(StringInfo out) +{ + if (out->len && out->data[out->len - 1] != ' ' && out->data[out->len - 1] != '(') + appendStringInfoChar(out, ' '); +} + +/* token kind permits Go automatic-semicolon-insertion at a line break */ +static bool +go_asi_kind(GoTok *tk) +{ + switch (tk->kind) + { + case GO_WORD: + case GO_NUM: + case GO_STR: + case GO_RP: + case GO_RBRACK: + case GO_RBRACE: + return true; + case GO_OP: + return (tk->len == 2 && (tk->s[0] == '+' || tk->s[0] == '-') && + tk->s[1] == tk->s[0]); /* ++ or -- */ + default: + return false; + } +} + +static void +go_lex(Go *g, const char *body) +{ + const char *p = body, + *end = body + strlen(body); + int line = 1, + cap = 256, + n = 0; + GoTok *t = palloc(sizeof(GoTok) * cap); + +#define GOPUSH(k, st, ln) do { \ + if (n >= cap) { cap *= 2; t = repalloc(t, sizeof(GoTok) * cap); } \ + t[n].kind = (k); t[n].s = (st); t[n].len = (ln); t[n].line = line; n++; \ + } while (0) +#define GOALPHA(c) (((c) >= 'A' && (c) <= 'Z') || ((c) >= 'a' && (c) <= 'z') || (c) == '_') +#define GODIGIT(c) ((c) >= '0' && (c) <= '9') + + while (p < end) + { + if (*p == '\n') + { + if (n > 0 && go_asi_kind(&t[n - 1])) /* ASI */ + { + GOPUSH(GO_SEMI, p, 1); + } + line++; + p++; + continue; + } + if (*p == ' ' || *p == '\t' || *p == '\r') + { + p++; + continue; + } + if (p[0] == '/' && p + 1 < end && p[1] == '/') /* // line comment */ + { + while (p < end && *p != '\n') + p++; + continue; + } + if (p[0] == '/' && p + 1 < end && p[1] == '*') /* block comment */ + { + p += 2; + while (p + 1 < end && !(p[0] == '*' && p[1] == '/')) + { + if (*p == '\n') + line++; + p++; + } + p += 2; + continue; + } + if (*p == '"') /* interpreted string */ + { + const char *st = p; + bool closed = false; + + p++; + while (p < end) + { + if (*p == '\\' && p + 1 < end) + { + p += 2; + continue; + } + if (*p == '"') + { + p++; + closed = true; + break; + } + if (*p == '\n') + break; + p++; + } + if (!closed) + plx_err(g->cx, line, "unterminated string literal"); + GOPUSH(GO_STR, st, (int) (p - st)); + continue; + } + if (*p == '`') /* raw string */ + { + const char *st = p; + + p++; + while (p < end && *p != '`') + { + if (*p == '\n') + line++; + p++; + } + if (p >= end) + plx_err(g->cx, line, "unterminated raw string literal"); + p++; + GOPUSH(GO_STR, st, (int) (p - st)); + continue; + } + if (*p == '\'') /* rune literal */ + { + const char *st = p; + + p++; + while (p < end && *p != '\'') + { + if (*p == '\\' && p + 1 < end) + p++; + p++; + } + if (p < end) + p++; + GOPUSH(GO_STR, st, (int) (p - st)); /* treated as a string */ + continue; + } + if (GOALPHA(*p)) + { + const char *st = p; + + while (p < end && (GOALPHA(*p) || GODIGIT(*p))) + p++; + GOPUSH(GO_WORD, st, (int) (p - st)); + continue; + } + if (GODIGIT(*p)) + { + const char *st = p; + + while (p < end && (GODIGIT(*p) || *p == '.' || *p == '_' || + *p == 'x' || *p == 'X' || + ((*p >= 'a' && *p <= 'f') || (*p >= 'A' && *p <= 'F')) || + (( *p == '+' || *p == '-') && (p[-1] == 'e' || p[-1] == 'E')))) + p++; + GOPUSH(GO_NUM, st, (int) (p - st)); + continue; + } + if (*p == '(') + { + GOPUSH(GO_LP, p, 1); + p++; + continue; + } + if (*p == ')') + { + GOPUSH(GO_RP, p, 1); + p++; + continue; + } + if (*p == '{') + { + GOPUSH(GO_LBRACE, p, 1); + p++; + continue; + } + if (*p == '}') + { + GOPUSH(GO_RBRACE, p, 1); + p++; + continue; + } + if (*p == '[') + { + GOPUSH(GO_LBRACK, p, 1); + p++; + continue; + } + if (*p == ']') + { + GOPUSH(GO_RBRACK, p, 1); + p++; + continue; + } + if (*p == ',') + { + GOPUSH(GO_COMMA, p, 1); + p++; + continue; + } + if (*p == ';') + { + GOPUSH(GO_SEMI, p, 1); + p++; + continue; + } + if (*p == '.' && !(p + 1 < end && GODIGIT(p[1]))) + { + if (p + 2 < end && p[1] == '.' && p[2] == '.') /* ... */ + { + GOPUSH(GO_OP, p, 3); + p += 3; + } + else + { + GOPUSH(GO_DOT, p, 1); + p++; + } + continue; + } + { + static const char *ops3[] = {"&^=", "<<=", ">>=", "...", NULL}; + static const char *ops2[] = { + ":=", "==", "!=", "<=", ">=", "&&", "||", "+=", "-=", + "*=", "/=", "%=", "++", "--", "<<", ">>", "&^", "<-", + "&=", "|=", "^=", NULL + }; + int k; + bool matched = false; + + for (k = 0; ops3[k]; k++) + if (p + 2 < end && strncmp(p, ops3[k], 3) == 0) + { + GOPUSH(GO_OP, ops3[k], 3); + p += 3; + matched = true; + break; + } + if (matched) + continue; + for (k = 0; ops2[k]; k++) + if (p + 1 < end && p[0] == ops2[k][0] && p[1] == ops2[k][1]) + { + GOPUSH(GO_OP, ops2[k], 2); + p += 2; + matched = true; + break; + } + if (matched) + continue; + } + GOPUSH(GO_OP, p, 1); + p++; + } + if (n > 0 && go_asi_kind(&t[n - 1])) /* final ASI */ + GOPUSH(GO_SEMI, end, 0); + GOPUSH(GO_EOF, end, 0); + g->t = t; + g->nt = n; + g->pos = 0; +#undef GOPUSH +#undef GOALPHA +#undef GODIGIT +} + +/* emit a Go string/rune literal as a SQL string literal */ +static void +go_emit_str(GoTok *tk, StringInfo out) +{ + char q = tk->s[0]; + const char *s = tk->s + 1, + *e = tk->s + tk->len - 1; + + appendStringInfoChar(out, '\''); + if (q == '`') /* raw: no escapes */ + { + while (s < e) + { + if (*s == '\'') + appendStringInfoString(out, "''"); + else + appendStringInfoChar(out, *s); + s++; + } + } + else /* "..." or '...': decode escapes */ + { + while (s < e) + { + if (*s == '\\' && s + 1 < e) + { + s++; + switch (*s) + { + case 'n': appendStringInfoChar(out, '\n'); break; + case 't': appendStringInfoChar(out, '\t'); break; + case 'r': appendStringInfoChar(out, '\r'); break; + case '\\': appendStringInfoChar(out, '\\'); break; + case '\'': appendStringInfoString(out, "''"); break; + case '"': appendStringInfoChar(out, '"'); break; + default: appendStringInfoChar(out, *s); break; + } + s++; + continue; + } + if (*s == '\'') + appendStringInfoString(out, "''"); + else + appendStringInfoChar(out, *s); + s++; + } + } + appendStringInfoChar(out, '\''); +} + +/* map a Go base type name to a PostgreSQL type; NULL if unknown */ +static const char * +go_base_type(const char *s, int len) +{ + struct + { + const char *go; + const char *pg; + } m[] = { + {"int", "integer"}, {"int8", "smallint"}, {"int16", "smallint"}, + {"int32", "integer"}, {"int64", "bigint"}, {"rune", "integer"}, + {"uint", "bigint"}, {"uint8", "smallint"}, {"byte", "smallint"}, + {"uint16", "integer"}, {"uint32", "bigint"}, {"uint64", "bigint"}, + {"float32", "real"}, {"float64", "double precision"}, + {"string", "text"}, {"bool", "boolean"}, + {NULL, NULL} + }; + int i; + + for (i = 0; m[i].go; i++) + if ((int) strlen(m[i].go) == len && strncmp(m[i].go, s, len) == 0) + return m[i].pg; + return NULL; +} + +/* read a Go type reference from the stream into dest, consuming its tokens */ +static void +go_read_type(Go *g, StringInfo dest) +{ + bool slice = false; + GoTok *w; + const char *base; + + while (go_cur(g)->kind == GO_OP && go_cur(g)->len == 1 && go_cur(g)->s[0] == '*') + g->pos++; /* pointer: drop the * */ + if (go_cur(g)->kind == GO_LBRACK) /* []T or [N]T -> T[] */ + { + g->pos++; + while (go_cur(g)->kind != GO_RBRACK && go_cur(g)->kind != GO_EOF) + g->pos++; /* skip an optional array length */ + if (go_cur(g)->kind == GO_RBRACK) + g->pos++; + slice = true; + } + w = go_cur(g); + if (w->kind != GO_WORD) + go_err(g, "expected a type name"); + if (go_ci(w, "map") || go_ci(w, "chan") || go_ci(w, "func") || + go_ci(w, "interface") || go_ci(w, "struct")) + go_err(g, "map, chan, func, interface, and struct types are not supported"); + base = go_base_type(w->s, w->len); + if (base) + appendStringInfoString(dest, base); + else if (g->t[g->pos + 1].kind == GO_DOT && go_ci(w, "time") && + go_ci(&g->t[g->pos + 2], "Time")) + { + appendStringInfoString(dest, "timestamp"); + g->pos += 2; + } + else + appendStringInfoString(dest, go_ident(w->s, w->len)); /* named/PG type */ + g->pos++; + if (slice) + appendStringInfoString(dest, "[]"); +} + +/* infer a PostgreSQL type from an initializer range [a,b); NULL if unknown */ +static const char * +go_infer_type(Go *g, int a, int b) +{ + if (b - a == 1) + { + GoTok *tk = &g->t[a]; + + if (tk->kind == GO_NUM) + { + int i; + + for (i = 0; i < tk->len; i++) + if (tk->s[i] == '.' || + ((tk->s[i] == 'e' || tk->s[i] == 'E') && tk->len > 2 && tk->s[1] != 'x')) + return "double precision"; + return "integer"; + } + if (tk->kind == GO_STR) + return "text"; + if (go_ci(tk, "true") || go_ci(tk, "false")) + return "boolean"; + return NULL; + } + /* []T{...} array literal */ + if (g->t[a].kind == GO_LBRACK && g->t[a + 1].kind == GO_RBRACK && + g->t[a + 2].kind == GO_WORD) + { + const char *el = go_base_type(g->t[a + 2].s, g->t[a + 2].len); + + if (el) + return psprintf("%s[]", el); + } + /* len(...) / cap(...) -> integer */ + if (go_ci(&g->t[a], "len") || go_ci(&g->t[a], "cap")) + return "integer"; + /* a type conversion T(x) whose T is a base type */ + if (g->t[a].kind == GO_WORD && g->t[a + 1].kind == GO_LP) + { + const char *tc = go_base_type(g->t[a].s, g->t[a].len); + + if (tc) + return tc; + } + return NULL; +} + +/* index of the ')' / '}' / ']' matching the opener at idx, within [.,limit) */ +static int +go_match(Go *g, int idx, int limit) +{ + int depth = 0, + i; + + for (i = idx; i < limit; i++) + { + GoKind k = g->t[i].kind; + + if (k == GO_LP || k == GO_LBRACE || k == GO_LBRACK) + depth++; + else if (k == GO_RP || k == GO_RBRACE || k == GO_RBRACK) + if (--depth == 0) + return i; + } + return -1; +} + +/* a stdlib / builtin / type-conversion call at index i; sets *ni on success */ +static bool +go_emit_call(Go *g, int i, int b, StringInfo out, int *ni) +{ + GoTok *tk = &g->t[i]; + int lp, + close; + const char *conv; + + /* qualified call: pkg.Method(...) */ + if (tk->kind == GO_WORD && i + 2 < b && g->t[i + 1].kind == GO_DOT && + g->t[i + 2].kind == GO_WORD && i + 3 < b && g->t[i + 3].kind == GO_LP) + { + GoTok *pkg = tk, + *meth = &g->t[i + 2]; + struct + { + const char *pkg; + const char *meth; + const char *pg; + } r[] = { + {"strings", "ToUpper", "upper"}, {"strings", "ToLower", "lower"}, + {"strings", "TrimSpace", "btrim"}, {"strings", "ReplaceAll", "replace"}, + {"math", "Abs", "abs"}, {"math", "Floor", "floor"}, + {"math", "Ceil", "ceil"}, {"math", "Sqrt", "sqrt"}, + {"math", "Pow", "power"}, {"math", "Max", "greatest"}, + {"math", "Min", "least"}, {"math", "Mod", "mod"}, + {"strconv", "Itoa", NULL}, {"strconv", "Atoi", NULL}, + {"time", "Now", "now"}, {"fmt", "Sprintf", "format"}, + {NULL, NULL, NULL} + }; + int k; + + lp = i + 3; + close = go_match(g, lp, b); + if (close < 0) + return false; + for (k = 0; r[k].pkg; k++) + if ((int) strlen(r[k].pkg) == pkg->len && + strncmp(r[k].pkg, pkg->s, pkg->len) == 0 && + (int) strlen(r[k].meth) == meth->len && + strncmp(r[k].meth, meth->s, meth->len) == 0) + { + /* strconv.Itoa(x) -> (x)::text ; strconv.Atoi(x) -> (x)::integer */ + if (r[k].pg == NULL) + { + bool toint = (meth->len == 4); /* "Atoi" */ + + go_sp(out); + appendStringInfoChar(out, '('); + go_emit_range(g, lp + 1, close, out); + appendStringInfo(out, ")::%s", toint ? "integer" : "text"); + *ni = close + 1; + return true; + } + /* strings.Contains(s, sub) -> (strpos(s, sub) > 0) */ + go_sp(out); + appendStringInfoString(out, r[k].pg); + appendStringInfoChar(out, '('); + go_emit_range(g, lp + 1, close, out); + appendStringInfoChar(out, ')'); + *ni = close + 1; + return true; + } + if (go_ci(pkg, "strings") && go_ci(meth, "Contains")) + { + int depth = 0, + comma = -1, + j; + + for (j = lp + 1; j < close; j++) + { + if (g->t[j].kind == GO_LP) + depth++; + else if (g->t[j].kind == GO_RP) + depth--; + else if (g->t[j].kind == GO_COMMA && depth == 0 && comma < 0) + comma = j; + } + if (comma < 0) + return false; + go_sp(out); + appendStringInfoString(out, "(strpos("); + go_emit_range(g, lp + 1, comma, out); + appendStringInfoChar(out, ','); + go_emit_range(g, comma + 1, close, out); + appendStringInfoString(out, ") > 0)"); + *ni = close + 1; + return true; + } + return false; + } + + if (tk->kind != GO_WORD || i + 1 >= b || g->t[i + 1].kind != GO_LP) + return false; + lp = i + 1; + close = go_match(g, lp, b); + if (close < 0) + return false; + + /* type conversion T(x) -> (x)::T for a base type T */ + conv = go_base_type(tk->s, tk->len); + if (conv) + { + go_sp(out); + appendStringInfoChar(out, '('); + go_emit_range(g, lp + 1, close, out); + appendStringInfo(out, ")::%s", conv); + *ni = close + 1; + return true; + } + /* len(x): element count for a slice local, else character length */ + if (go_ci(tk, "len")) + { + PlxLocal2 *a = (close == lp + 2 && g->t[lp + 1].kind == GO_WORD) + ? plx_local_find(g->cx, g->t[lp + 1].s, g->t[lp + 1].len) : NULL; + bool isarr = a && a->typ && strlen(a->typ) > 2 && + strcmp(a->typ + strlen(a->typ) - 2, "[]") == 0; + + go_sp(out); + appendStringInfoString(out, isarr ? "coalesce(cardinality(" : "length("); + go_emit_range(g, lp + 1, close, out); + appendStringInfoString(out, isarr ? "), 0)" : ")"); + *ni = close + 1; + return true; + } + if (go_ci(tk, "append")) + { + go_sp(out); + appendStringInfoString(out, "array_append("); + go_emit_range(g, lp + 1, close, out); + appendStringInfoChar(out, ')'); + *ni = close + 1; + return true; + } + return false; +} + +/* emit tokens [a,b) as a plpgsql expression */ +static void +go_emit_range(Go *g, int a, int b, StringInfo out) +{ + int i = a; + + while (i < b) + { + GoTok *tk = &g->t[i]; + int ni; + + /* []T{ ... } array literal -> ARRAY[ ... ] */ + if (tk->kind == GO_LBRACK && i + 1 < b && g->t[i + 1].kind == GO_RBRACK && + i + 2 < b && g->t[i + 2].kind == GO_WORD && + i + 3 < b && g->t[i + 3].kind == GO_LBRACE) + { + int close = go_match(g, i + 3, b); + + if (close > 0) + { + go_sp(out); + appendStringInfoString(out, "ARRAY["); + go_emit_range(g, i + 4, close, out); + appendStringInfoChar(out, ']'); + i = close + 1; + continue; + } + } + if (tk->kind == GO_WORD) + { + if (go_emit_call(g, i, b, out, &ni)) + { + i = ni; + continue; + } + if (go_ci(tk, "nil")) + { + go_sp(out); + appendStringInfoString(out, "NULL"); + i++; + continue; + } + go_sp(out); + appendBinaryStringInfo(out, tk->s, tk->len); + i++; + continue; + } + if (tk->kind == GO_NUM) + { + go_sp(out); + appendBinaryStringInfo(out, tk->s, tk->len); + i++; + continue; + } + if (tk->kind == GO_STR) + { + go_sp(out); + go_emit_str(tk, out); + i++; + continue; + } + if (tk->kind == GO_LP) + { + go_sp(out); + appendStringInfoChar(out, '('); + i++; + continue; + } + if (tk->kind == GO_RP) + { + appendStringInfoChar(out, ')'); + i++; + continue; + } + if (tk->kind == GO_LBRACK) + { + /* + * A subscript a[e] on a value: Go slices are 0-based but PostgreSQL + * arrays are 1-based, so emit a[(e) + 1]. (An empty [] is a slice + * literal, handled above; a [ opening at the very start of a range + * is not a subscript.) + */ + if (i > a && i + 1 < b && g->t[i + 1].kind != GO_RBRACK && + (g->t[i - 1].kind == GO_WORD || g->t[i - 1].kind == GO_RP || + g->t[i - 1].kind == GO_RBRACK)) + { + int close = go_match(g, i, b); + + if (close > 0) + { + appendStringInfoString(out, "[("); + go_emit_range(g, i + 1, close, out); + appendStringInfoString(out, ") + 1]"); + i = close + 1; + continue; + } + } + appendStringInfoChar(out, '['); + i++; + continue; + } + if (tk->kind == GO_RBRACK) + { + appendStringInfoChar(out, ']'); + i++; + continue; + } + if (tk->kind == GO_COMMA) + { + appendStringInfoChar(out, ','); + i++; + continue; + } + if (tk->kind == GO_DOT) + { + appendStringInfoChar(out, '.'); + i++; + continue; + } + if (tk->kind == GO_OP) + { + if (tk->len == 2 && tk->s[0] == '=' && tk->s[1] == '=') + { + go_sp(out); + appendStringInfoChar(out, '='); + } + else if (tk->len == 2 && tk->s[0] == '!' && tk->s[1] == '=') + { + go_sp(out); + appendStringInfoString(out, "<>"); + } + else if (tk->len == 2 && tk->s[0] == '&' && tk->s[1] == '&') + { + go_sp(out); + appendStringInfoString(out, "AND"); + } + else if (tk->len == 2 && tk->s[0] == '|' && tk->s[1] == '|') + { + go_sp(out); + appendStringInfoString(out, "OR"); + } + else if (tk->len == 1 && tk->s[0] == '!') + { + go_sp(out); + appendStringInfoString(out, "NOT "); + } + else + { + go_sp(out); + appendBinaryStringInfo(out, tk->s, tk->len); + } + i++; + continue; + } + i++; + } +} + +/* end of a simple statement: the next top-level ; (brackets/braces nest) */ +static int +go_stmt_end(Go *g, int from) +{ + int i = from, + depth = 0; + + while (i < g->nt) + { + GoKind k = g->t[i].kind; + + if (k == GO_LP || k == GO_LBRACE || k == GO_LBRACK) + depth++; + else if (k == GO_RP || k == GO_RBRACE || k == GO_RBRACK) + { + if (depth == 0) + return i; + depth--; + } + else if (depth == 0 && (k == GO_SEMI || k == GO_EOF)) + return i; + i++; + } + return g->nt - 1; +} + +/* end of an if/for/switch header: the first top-level { that opens the body */ +static int +go_header_end(Go *g, int from) +{ + int i = from, + depth = 0; + + while (i < g->nt) + { + GoKind k = g->t[i].kind; + + if (k == GO_LP || k == GO_LBRACK) + depth++; + else if (k == GO_RP || k == GO_RBRACK) + depth--; + else if (depth == 0 && k == GO_LBRACE) + return i; + else if (depth == 0 && k == GO_EOF) + return i; + i++; + } + return g->nt - 1; +} + +static void +go_eat_semi(Go *g) +{ + while (go_cur(g)->kind == GO_SEMI) + g->pos++; +} + +/* declare a name with an inferred/explicit type and optional initializer */ +static void +go_declare_one(Go *g, const char *name, StringInfo typ, int e0, int e1, bool isconst, int ind) +{ + PlxLocal2 *l = plx_local_add(g->cx, name, (int) strlen(name)); + + if (typ && typ->len) + l->typ = pstrdup(typ->data); + else if (e1 > e0) + { + const char *inf = go_infer_type(g, e0, e1); + + if (!inf) + plx_err(g->cx, g->t[e0].line, + "cannot infer a PostgreSQL type for \"%s\"; declare it with an explicit type (var %s T)", + name, name); + l->typ = pstrdup(inf); + } + /* a const with no initializer would leave l->init NULL (invalid) */ + if (isconst && e1 <= e0) + plx_err(g->cx, g->t[e0 < g->nt ? e0 : g->nt - 1].line, + "a constant needs a value (const %s = ...)", name); + l->is_const = isconst; + if (e1 > e0) + { + if (isconst) + { + /* a constant's value lives in the DECLARE, not a runtime assignment */ + StringInfoData v; + + initStringInfo(&v); + go_emit_range(g, e0, e1, &v); + l->init = v.data; + } + else + { + plx_indent(&g->cx->out, ind); + appendStringInfo(&g->cx->out, "%s :=", name); + go_emit_range(g, e0, e1, &g->cx->out); + appendStringInfoString(&g->cx->out, ";\n"); + } + } +} + +/* var x T [= e] | var x = e | var ( ... ) | const ... */ +static void +go_var(Go *g, int ind, bool isconst) +{ + g->pos++; /* var / const */ + if (go_cur(g)->kind == GO_LP) /* grouped var ( ... ) */ + { + g->pos++; + go_eat_semi(g); + while (go_cur(g)->kind != GO_RP && go_cur(g)->kind != GO_EOF) + { + GoTok *nm = go_cur(g); + StringInfoData ty; + int e0 = -1, + e1 = -1; + + if (nm->kind != GO_WORD) + go_err(g, "expected a name in var block"); + g->pos++; + initStringInfo(&ty); + if (go_cur(g)->kind != GO_OP || go_cur(g)->s[0] != '=') + go_read_type(g, &ty); + if (go_cur(g)->kind == GO_OP && go_cur(g)->len == 1 && go_cur(g)->s[0] == '=') + { + g->pos++; + e0 = g->pos; + e1 = go_stmt_end(g, e0); + g->pos = e1; + } + go_declare_one(g, go_ident(nm->s, nm->len), &ty, e0 < 0 ? 0 : e0, + e1 < 0 ? 0 : e1, isconst, ind); + go_eat_semi(g); + } + if (go_cur(g)->kind == GO_RP) + g->pos++; + go_eat_semi(g); + return; + } + { + GoTok *nm = go_cur(g); + StringInfoData ty; + int e0 = 0, + e1 = 0; + + if (nm->kind != GO_WORD) + go_err(g, "expected a name after var"); + g->pos++; + initStringInfo(&ty); + if (!(go_cur(g)->kind == GO_OP && go_cur(g)->len == 1 && go_cur(g)->s[0] == '=')) + go_read_type(g, &ty); + if (go_cur(g)->kind == GO_OP && go_cur(g)->len == 1 && go_cur(g)->s[0] == '=') + { + g->pos++; + e0 = g->pos; + e1 = go_stmt_end(g, e0); + g->pos = e1; + } + go_declare_one(g, go_ident(nm->s, nm->len), &ty, e0, e1, isconst, ind); + go_eat_semi(g); + } +} + +/* an assignment, short declaration, ++/--, or expression statement */ +static void +go_simple(Go *g, int ind, int stop) +{ + int e0 = g->pos, + e1 = (stop >= 0) ? stop : go_stmt_end(g, e0); + int i, + depth = 0, + opidx = -1; + char opc = 0; + bool shortdecl = false; + + /* an empty statement range means a stray closer/terminator (e.g. ')' or ']') + * reached statement position; error rather than spin without advancing */ + if (e1 <= e0) + go_err(g, "unexpected token"); + + /* find the top-level assignment operator, if any (each is a single token) */ + for (i = e0; i < e1; i++) + { + GoKind k = g->t[i].kind; + + if (k == GO_LP || k == GO_LBRACE || k == GO_LBRACK) + depth++; + else if (k == GO_RP || k == GO_RBRACE || k == GO_RBRACK) + depth--; + else if (depth == 0 && k == GO_OP) + { + GoTok *o = &g->t[i]; + + if (o->len == 2 && o->s[0] == ':' && o->s[1] == '=') + { + opidx = i; + shortdecl = true; + break; + } + if (o->len == 1 && o->s[0] == '=') + { + opidx = i; + break; + } + if (o->len == 2 && o->s[1] == '=' && strchr("+-*/%", o->s[0])) + { + opidx = i; + opc = o->s[0]; + break; + } + } + } + + /* ++ / -- */ + if (opidx < 0 && e1 - e0 >= 2 && g->t[e1 - 1].kind == GO_OP && + g->t[e1 - 1].len == 2 && + (g->t[e1 - 1].s[0] == '+' || g->t[e1 - 1].s[0] == '-') && + g->t[e1 - 1].s[1] == g->t[e1 - 1].s[0]) + { + plx_indent(&g->cx->out, ind); + go_emit_range(g, e0, e1 - 1, &g->cx->out); + appendStringInfoString(&g->cx->out, " := "); + go_emit_range(g, e0, e1 - 1, &g->cx->out); + appendStringInfo(&g->cx->out, " %c 1;\n", g->t[e1 - 1].s[0]); + g->pos = e1; + return; + } + + if (opidx < 0) + { + /* an expression statement: a call for its effect */ + GoTok *h = &g->t[e0]; + + plx_indent(&g->cx->out, ind); + if (go_ci(h, "panic")) + { + int lp = e0 + 1, + close = (g->t[lp].kind == GO_LP) ? go_match(g, lp, e1) : -1; + + if (close > lp + 1) + { + appendStringInfoString(&g->cx->out, "RAISE EXCEPTION '%',"); + go_emit_range(g, lp + 1, close, &g->cx->out); + appendStringInfoString(&g->cx->out, ";\n"); + } + else + appendStringInfoString(&g->cx->out, "RAISE EXCEPTION 'panic';\n"); + } + else if (go_ci(h, "fmt") && g->t[e0 + 1].kind == GO_DOT) + { + GoTok *m = &g->t[e0 + 2]; + int lp = e0 + 3, + close = (lp < g->nt && g->t[lp].kind == GO_LP) + ? go_match(g, lp, e1) : -1; + int args0 = lp + 1; + + /* Printf/Sprintf: the first argument is the format string; the rest + * are the values to raise (SQL RAISE has no printf verbs, so the + * format's literal text and directives are dropped) */ + if (close > lp + 1 && (go_ci(m, "Printf") || go_ci(m, "Sprintf"))) + { + int d = 0, + comma = -1, + j; + + for (j = lp + 1; j < close; j++) + { + if (g->t[j].kind == GO_LP) + d++; + else if (g->t[j].kind == GO_RP) + d--; + else if (g->t[j].kind == GO_COMMA && d == 0 && comma < 0) + comma = j; + } + if (comma > 0) + args0 = comma + 1; + } + if (close > args0) + { + /* one % placeholder per top-level argument */ + int d = 0, + nargs = 1, + j, + k; + + for (j = args0; j < close; j++) + { + GoKind kk = g->t[j].kind; + + if (kk == GO_LP || kk == GO_LBRACK || kk == GO_LBRACE) + d++; + else if (kk == GO_RP || kk == GO_RBRACK || kk == GO_RBRACE) + d--; + else if (kk == GO_COMMA && d == 0) + nargs++; + } + appendStringInfoString(&g->cx->out, "RAISE NOTICE '"); + for (k = 0; k < nargs; k++) + appendStringInfoString(&g->cx->out, k ? " %" : "%"); + appendStringInfoString(&g->cx->out, "',"); + go_emit_range(g, args0, close, &g->cx->out); + appendStringInfoString(&g->cx->out, ";\n"); + } + else + appendStringInfoString(&g->cx->out, "RAISE NOTICE '';\n"); + } + else if (go_ci(h, "emit")) + { + int lp = e0 + 1, + close = (g->t[lp].kind == GO_LP) ? go_match(g, lp, e1) : -1; + + g->cx->retset = true; + if (close > lp + 1) + { + appendStringInfoString(&g->cx->out, "RETURN NEXT"); + go_emit_range(g, lp + 1, close, &g->cx->out); + appendStringInfoString(&g->cx->out, ";\n"); + } + else + appendStringInfoString(&g->cx->out, "RETURN NEXT;\n"); + } + else if (go_ci(h, "execute") || go_ci(h, "perform")) + { + int lp = e0 + 1, + close = (g->t[lp].kind == GO_LP) ? go_match(g, lp, e1) : -1; + + appendStringInfoString(&g->cx->out, + go_ci(h, "execute") ? "EXECUTE" : "PERFORM"); + if (close > 0) + go_emit_range(g, lp + 1, close, &g->cx->out); + appendStringInfoString(&g->cx->out, ";\n"); + } + else + { + appendStringInfoString(&g->cx->out, "PERFORM"); + go_emit_range(g, e0, e1, &g->cx->out); + appendStringInfoString(&g->cx->out, ";\n"); + } + g->pos = e1; + return; + } + + /* short declaration: declare each new name (types inferred) */ + if (shortdecl) + { + int names[32], + nnames = 0, + j, + d = 0; + + for (j = e0; j < opidx; j++) + { + if (g->t[j].kind == GO_WORD && nnames < 32) + names[nnames++] = j; + } + /* single target: infer from the whole RHS */ + if (nnames == 1) + { + char *nm = go_ident(g->t[names[0]].s, g->t[names[0]].len); + + if (!plx_local_find(g->cx, nm, (int) strlen(nm))) + go_declare_one(g, nm, NULL, opidx + 1, e1, false, ind); + else + { + plx_indent(&g->cx->out, ind); + appendStringInfo(&g->cx->out, "%s :=", nm); + go_emit_range(g, opidx + 1, e1, &g->cx->out); + appendStringInfoString(&g->cx->out, ";\n"); + } + g->pos = e1; + return; + } + /* multiple targets a, b := x, y : split RHS on top-level commas. + * rhs holds (start,end) pairs, so it needs 2 ints per target. */ + { + int rhs[64], + nrhs = 0, + start = opidx + 1; + + d = 0; + for (j = opidx + 1; j <= e1; j++) + { + GoKind k = (j < e1) ? g->t[j].kind : GO_COMMA; + + if (k == GO_LP || k == GO_LBRACE || k == GO_LBRACK) + d++; + else if (k == GO_RP || k == GO_RBRACE || k == GO_RBRACK) + d--; + else if (k == GO_COMMA && d == 0 && nrhs < 32) + { + rhs[nrhs * 2] = start; + rhs[nrhs * 2 + 1] = j; + nrhs++; + start = j + 1; + } + } + for (j = 0; j < nnames && j < nrhs; j++) + { + char *nm = go_ident(g->t[names[j]].s, g->t[names[j]].len); + + if (!plx_local_find(g->cx, nm, (int) strlen(nm))) + go_declare_one(g, nm, NULL, rhs[j * 2], rhs[j * 2 + 1], false, ind); + else + { + plx_indent(&g->cx->out, ind); + appendStringInfo(&g->cx->out, "%s :=", nm); + go_emit_range(g, rhs[j * 2], rhs[j * 2 + 1], &g->cx->out); + appendStringInfoString(&g->cx->out, ";\n"); + } + } + } + g->pos = e1; + return; + } + + /* parallel assignment a, b = x, y : SELECT x, y INTO a, b (RHS eval first) */ + if (!opc) + { + int d = 0, + j, + commas = 0; + + for (j = e0; j < opidx; j++) + { + GoKind k = g->t[j].kind; + + if (k == GO_LP || k == GO_LBRACK || k == GO_LBRACE) + d++; + else if (k == GO_RP || k == GO_RBRACK || k == GO_RBRACE) + d--; + else if (k == GO_COMMA && d == 0) + commas++; + } + if (commas > 0) + { + plx_indent(&g->cx->out, ind); + appendStringInfoString(&g->cx->out, "SELECT"); + go_emit_range(g, opidx + 1, e1, &g->cx->out); + appendStringInfoString(&g->cx->out, " INTO"); + go_emit_range(g, e0, opidx, &g->cx->out); + appendStringInfoString(&g->cx->out, ";\n"); + g->pos = e1; + return; + } + } + + /* plain assignment: LHS RHS */ + plx_indent(&g->cx->out, ind); + go_emit_range(g, e0, opidx, &g->cx->out); + appendStringInfoString(&g->cx->out, " :="); + if (opc) + { + go_emit_range(g, e0, opidx, &g->cx->out); + appendStringInfo(&g->cx->out, " %c (", opc); + go_emit_range(g, opidx + 1, e1, &g->cx->out); + appendStringInfoString(&g->cx->out, ");\n"); + } + else + { + go_emit_range(g, opidx + 1, e1, &g->cx->out); + appendStringInfoString(&g->cx->out, ";\n"); + } + g->pos = e1; +} + +/* if [init;] cond { } [else if ...] [else { }] */ +static void +go_if(Go *g, int ind) +{ + int hb, + c0, + semi = -1, + i, + depth = 0; + + /* guard the else-if chain, which recurses into go_if directly (bypassing + * go_stmt's depth counter) */ + if (++g->cx->depth > PLX_MAX_DEPTH) + go_err(g, "if/else nested too deeply"); + + g->pos++; /* if */ + hb = go_header_end(g, g->pos); + /* an optional init statement precedes a top-level ; in the header */ + for (i = g->pos; i < hb; i++) + { + GoKind k = g->t[i].kind; + + if (k == GO_LP || k == GO_LBRACK) + depth++; + else if (k == GO_RP || k == GO_RBRACK) + depth--; + else if (depth == 0 && k == GO_SEMI) + semi = i; + } + if (semi >= 0) + { + go_simple(g, ind, semi); + g->pos = semi + 1; + } + c0 = g->pos; + plx_indent(&g->cx->out, ind); + appendStringInfoString(&g->cx->out, "IF"); + go_emit_range(g, c0, hb, &g->cx->out); + appendStringInfoString(&g->cx->out, " THEN\n"); + g->pos = hb; + go_block(g, ind + 1); + go_eat_semi(g); + if (go_ci(go_cur(g), "else")) + { + g->pos++; + if (go_ci(go_cur(g), "if")) + { + plx_indent(&g->cx->out, ind); + appendStringInfoString(&g->cx->out, "ELSE\n"); + go_if(g, ind + 1); + } + else + { + plx_indent(&g->cx->out, ind); + appendStringInfoString(&g->cx->out, "ELSE\n"); + go_block(g, ind + 1); + } + go_eat_semi(g); + } + plx_indent(&g->cx->out, ind); + appendStringInfoString(&g->cx->out, "END IF;\n"); + g->cx->depth--; +} + +/* for { } | for cond { } | for init; cond; post { } | for k,v := range e { } */ +static void +go_for(Go *g, int ind) +{ + int hb, + h0, + semi1 = -1, + semi2 = -1, + i, + depth = 0, + rangepos = -1; + + g->pos++; /* for */ + h0 = g->pos; + hb = go_header_end(g, g->pos); + for (i = h0; i < hb; i++) + { + GoKind k = g->t[i].kind; + + if (k == GO_LP || k == GO_LBRACK || k == GO_LBRACE) + depth++; + else if (k == GO_RP || k == GO_RBRACK || k == GO_RBRACE) + depth--; + else if (depth == 0 && k == GO_SEMI) + { + if (semi1 < 0) + semi1 = i; + else + semi2 = i; + } + else if (depth == 0 && go_ci(&g->t[i], "range")) + rangepos = i; + } + + /* for k, v := range e -> FOREACH / FOR ... IN */ + if (rangepos >= 0) + { + int assign = -1, + j; + int rs = rangepos + 1; /* the range expression start */ + char *valname = NULL, + *idxname = NULL; + + for (j = h0; j < rangepos; j++) + if (g->t[j].kind == GO_OP && + ((g->t[j].len == 2 && g->t[j].s[0] == ':' && g->t[j].s[1] == '=') || + (g->t[j].len == 1 && g->t[j].s[0] == '='))) + assign = j; + if (assign > h0) + { + /* names between h0 and assign: one (value) or two (index, value) */ + int nm[4], + nn = 0; + + for (j = h0; j < assign; j++) + if (g->t[j].kind == GO_WORD && nn < 4) + nm[nn++] = j; + /* Go: a single var is the index; two vars are (index, value) */ + if (nn == 1) + { + if (!(g->t[nm[0]].len == 1 && g->t[nm[0]].s[0] == '_')) + idxname = go_ident(g->t[nm[0]].s, g->t[nm[0]].len); + } + else if (nn >= 2) + { + if (!(g->t[nm[0]].len == 1 && g->t[nm[0]].s[0] == '_')) + idxname = go_ident(g->t[nm[0]].s, g->t[nm[0]].len); + if (!(g->t[nm[1]].len == 1 && g->t[nm[1]].s[0] == '_')) + valname = go_ident(g->t[nm[1]].s, g->t[nm[1]].len); + } + } + /* range query("...") -> FOR row IN LOOP */ + if (go_ci(&g->t[rs], "query") && g->t[rs + 1].kind == GO_LP) + { + int close = go_match(g, rs + 1, hb); + char *rowname = valname ? valname : idxname; + + if (!rowname) + go_err(g, "range over query() needs a row variable (for _, row := range query(...))"); + if (!plx_local_find(g->cx, rowname, (int) strlen(rowname))) + { + PlxLocal2 *l = plx_local_add(g->cx, rowname, (int) strlen(rowname)); + + l->is_record = true; + } + plx_indent(&g->cx->out, ind); + appendStringInfo(&g->cx->out, "FOR %s IN EXECUTE", rowname); + go_emit_range(g, rs + 2, close, &g->cx->out); + appendStringInfoString(&g->cx->out, " LOOP\n"); + g->pos = hb; + g->cx->loopdepth++; + go_block(g, ind + 1); + g->cx->loopdepth--; + plx_indent(&g->cx->out, ind); + appendStringInfoString(&g->cx->out, "END LOOP;\n"); + go_eat_semi(g); + return; + } + /* both index and value: FOREACH exposes only the value, so reject */ + if (idxname && valname) + go_err(g, "for index, value := range is not supported; use for _, v := range (value) or for i := range (index)"); + /* single var: the index -> FOR i IN 0 .. n-1 (Go 1.22 integer range) */ + if (idxname) + { + if (!plx_local_find(g->cx, idxname, (int) strlen(idxname))) + { + PlxLocal2 *l = plx_local_add(g->cx, idxname, (int) strlen(idxname)); + + l->typ = pstrdup("integer"); + } + plx_indent(&g->cx->out, ind); + appendStringInfo(&g->cx->out, "FOR %s IN 0 .. (", idxname); + go_emit_range(g, rs, hb, &g->cx->out); + appendStringInfoString(&g->cx->out, ") - 1 LOOP\n"); + g->pos = hb; + g->cx->loopdepth++; + go_block(g, ind + 1); + g->cx->loopdepth--; + plx_indent(&g->cx->out, ind); + appendStringInfoString(&g->cx->out, "END LOOP;\n"); + go_eat_semi(g); + return; + } + /* range over a slice -> FOREACH v IN ARRAY e */ + if (!valname) + go_err(g, "range needs a value variable (for _, v := range s)"); + if (!plx_local_find(g->cx, valname, (int) strlen(valname))) + { + /* infer the element type from a plain slice local (var s []T) */ + PlxLocal2 *arr = (rs + 1 == hb && g->t[rs].kind == GO_WORD) + ? plx_local_find(g->cx, g->t[rs].s, g->t[rs].len) : NULL; + + if (arr && arr->typ) + { + int tl = (int) strlen(arr->typ); + + if (tl > 2 && strcmp(arr->typ + tl - 2, "[]") == 0) + { + PlxLocal2 *l = plx_local_add(g->cx, valname, (int) strlen(valname)); + + l->typ = pnstrdup(arr->typ, tl - 2); + } + else + go_err(g, "declare the range value variable with var before the loop"); + } + else + go_err(g, "declare the range value variable with var before the loop"); + } + plx_indent(&g->cx->out, ind); + appendStringInfo(&g->cx->out, "FOREACH %s IN ARRAY", valname); + go_emit_range(g, rs, hb, &g->cx->out); + appendStringInfoString(&g->cx->out, " LOOP\n"); + g->pos = hb; + g->cx->loopdepth++; + go_block(g, ind + 1); + g->cx->loopdepth--; + plx_indent(&g->cx->out, ind); + appendStringInfoString(&g->cx->out, "END LOOP;\n"); + go_eat_semi(g); + return; + } + + /* for init; cond; post { } */ + if (semi1 >= 0 && semi2 >= 0) + { + /* + * The canonical counting loop -- for i := A; i < B; i++ (or <=, or + * i += S) -- becomes a plpgsql integer FOR. This is not just tidier: + * plpgsql CONTINUE jumps to the loop's own increment, whereas in a + * hand-built WHILE it would skip the post statement and loop forever. + */ + int ivar = -1; + bool inclusive = false; + char dir = 0; /* 'u' counts up (++/+=), 'd' down (--/-=) */ + char condc = 0; /* comparison direction: '<' or '>' */ + int step0 = -1, + step1 = -1; + + /* init: :=|= */ + if (g->t[h0].kind == GO_WORD && h0 + 2 <= semi1 && + g->t[h0 + 1].kind == GO_OP && + ((g->t[h0 + 1].len == 2 && g->t[h0 + 1].s[0] == ':' && g->t[h0 + 1].s[1] == '=') || + (g->t[h0 + 1].len == 1 && g->t[h0 + 1].s[0] == '='))) + ivar = h0; + /* post: ++ / -- / += S / -= S sets the direction */ + if (ivar >= 0 && g->t[semi2 + 1].kind == GO_WORD && + g->t[semi2 + 1].len == g->t[ivar].len && + strncmp(g->t[semi2 + 1].s, g->t[ivar].s, g->t[ivar].len) == 0 && + g->t[semi2 + 2].kind == GO_OP && g->t[semi2 + 2].len == 2) + { + GoTok *po = &g->t[semi2 + 2]; + + if (po->s[0] == '+' && po->s[1] == '+') + dir = 'u'; + else if (po->s[0] == '-' && po->s[1] == '-') + dir = 'd'; + else if (po->s[0] == '+' && po->s[1] == '=') + { + dir = 'u'; + step0 = semi2 + 3; + step1 = hb; + } + else if (po->s[0] == '-' && po->s[1] == '=') + { + dir = 'd'; + step0 = semi2 + 3; + step1 = hb; + } + } + if (!dir) + ivar = -1; + /* cond: />= (down), comparison immediately after var */ + if (ivar >= 0 && g->t[semi1 + 1].kind == GO_WORD && + g->t[semi1 + 1].len == g->t[ivar].len && + strncmp(g->t[semi1 + 1].s, g->t[ivar].s, g->t[ivar].len) == 0 && + g->t[semi1 + 2].kind == GO_OP && + (g->t[semi1 + 2].s[0] == '<' || g->t[semi1 + 2].s[0] == '>') && + (g->t[semi1 + 2].len == 1 || + (g->t[semi1 + 2].len == 2 && g->t[semi1 + 2].s[1] == '='))) + { + condc = g->t[semi1 + 2].s[0]; + inclusive = (g->t[semi1 + 2].len == 2); + } + else + ivar = -1; + /* the comparison must agree with the increment direction */ + if (ivar >= 0 && ((dir == 'u' && condc != '<') || (dir == 'd' && condc != '>'))) + ivar = -1; + + if (ivar >= 0) + { + plx_indent(&g->cx->out, ind); + appendStringInfo(&g->cx->out, "FOR %s IN%s", go_ident(g->t[ivar].s, g->t[ivar].len), + dir == 'd' ? " REVERSE" : ""); + go_emit_range(g, ivar + 2, semi1, &g->cx->out); /* A (loop start) */ + appendStringInfoString(&g->cx->out, " .."); + if (inclusive) + go_emit_range(g, semi1 + 3, semi2, &g->cx->out); /* B (bound) */ + else + { + appendStringInfoString(&g->cx->out, " ("); + go_emit_range(g, semi1 + 3, semi2, &g->cx->out); /* B */ + appendStringInfo(&g->cx->out, ") %c 1", dir == 'd' ? '+' : '-'); + } + if (step0 >= 0) + { + if (step1 <= step0) + go_err(g, "for loop step (+=/-=) is missing its amount"); + appendStringInfoString(&g->cx->out, " BY"); + go_emit_range(g, step0, step1, &g->cx->out); + } + appendStringInfoString(&g->cx->out, " LOOP\n"); + g->pos = hb; + g->cx->loopdepth++; + go_block(g, ind + 1); + g->cx->loopdepth--; + plx_indent(&g->cx->out, ind); + appendStringInfoString(&g->cx->out, "END LOOP;\n"); + go_eat_semi(g); + return; + } + } + + /* general C-style for -> [init;] WHILE cond LOOP body post; END LOOP. + * (continue re-tests the condition without running the post statement, as + * in plpgsql; use the counting form above when that matters.) */ + if (semi1 >= 0) + { + if (semi1 > h0) /* init statement */ + { + go_simple(g, ind, semi1); + } + g->pos = hb; + plx_indent(&g->cx->out, ind); + appendStringInfoString(&g->cx->out, "WHILE"); + if (semi2 > semi1 + 1) + go_emit_range(g, semi1 + 1, semi2, &g->cx->out); + else + appendStringInfoString(&g->cx->out, " true"); + appendStringInfoString(&g->cx->out, " LOOP\n"); + go_block(g, ind + 1); + if (semi2 >= 0 && hb > semi2 + 1) /* post statement */ + { + int save = g->pos; + + g->pos = semi2 + 1; + go_simple(g, ind + 1, hb); + g->pos = save; + } + plx_indent(&g->cx->out, ind); + appendStringInfoString(&g->cx->out, "END LOOP;\n"); + go_eat_semi(g); + return; + } + + /* for { } (infinite) or for cond { } (while) */ + plx_indent(&g->cx->out, ind); + if (hb > h0) + { + appendStringInfoString(&g->cx->out, "WHILE"); + go_emit_range(g, h0, hb, &g->cx->out); + appendStringInfoString(&g->cx->out, " LOOP\n"); + } + else + appendStringInfoString(&g->cx->out, "LOOP\n"); + g->pos = hb; + g->cx->loopdepth++; + go_block(g, ind + 1); + g->cx->loopdepth--; + plx_indent(&g->cx->out, ind); + appendStringInfoString(&g->cx->out, "END LOOP;\n"); + go_eat_semi(g); +} + +/* switch [tag] { case a, b: ...; default: ... } -> IF/ELSIF/ELSE */ +static void +go_switch(Go *g, int ind) +{ + int hb, + t0, + tag0 = -1, + tag1 = -1; + bool first = true, + had_default = false; + + g->pos++; /* switch */ + t0 = g->pos; + hb = go_header_end(g, g->pos); + if (hb > t0) /* a switch tag is present */ + { + tag0 = t0; + tag1 = hb; + } + if (go_cur(g)->kind != GO_LBRACE && g->t[hb].kind == GO_LBRACE) + g->pos = hb; + if (go_cur(g)->kind != GO_LBRACE) + go_err(g, "expected { after switch"); + g->pos++; /* { */ + go_eat_semi(g); + while (go_cur(g)->kind != GO_RBRACE && go_cur(g)->kind != GO_EOF) + { + bool is_default = go_ci(go_cur(g), "default"); + + if (!go_ci(go_cur(g), "case") && !is_default) + go_err(g, "expected case or default in switch"); + g->pos++; /* case / default */ + if (!is_default) + { + /* case values up to the ':' ; multiple values are OR-ed */ + int cv0 = g->pos, + colon = -1, + j, + depth = 0; + + for (j = g->pos; j < g->nt; j++) + { + GoKind k = g->t[j].kind; + + if (k == GO_LP || k == GO_LBRACK || k == GO_LBRACE) + depth++; + else if (k == GO_RP || k == GO_RBRACK || k == GO_RBRACE) + depth--; + else if (depth == 0 && k == GO_OP && g->t[j].len == 1 && g->t[j].s[0] == ':') + { + colon = j; + break; + } + } + if (colon < 0) + go_err(g, "expected : after case"); + plx_indent(&g->cx->out, ind); + appendStringInfoString(&g->cx->out, first ? "IF" : "ELSIF"); + /* split the case values on top-level commas */ + { + int s = cv0, + d = 0, + nc = 0; + + for (j = cv0; j <= colon; j++) + { + GoKind k = (j < colon) ? g->t[j].kind : GO_COMMA; + + if (k == GO_LP || k == GO_LBRACK) + d++; + else if (k == GO_RP || k == GO_RBRACK) + d--; + else if (k == GO_COMMA && d == 0) + { + if (nc) + appendStringInfoString(&g->cx->out, " OR"); + if (tag0 >= 0) + { + appendStringInfoChar(&g->cx->out, ' '); + go_emit_range(g, tag0, tag1, &g->cx->out); + appendStringInfoString(&g->cx->out, " ="); + } + go_emit_range(g, s, j, &g->cx->out); + s = j + 1; + nc++; + } + } + } + appendStringInfoString(&g->cx->out, " THEN\n"); + g->pos = colon + 1; + first = false; + } + else + { + int colon = -1, + j; + + for (j = g->pos; j < g->nt; j++) + if (g->t[j].kind == GO_OP && g->t[j].len == 1 && g->t[j].s[0] == ':') + { + colon = j; + break; + } + if (colon < 0) + go_err(g, "expected : after default"); + plx_indent(&g->cx->out, ind); + appendStringInfoString(&g->cx->out, first ? "IF true THEN\n" : "ELSE\n"); + g->pos = colon + 1; + had_default = true; + first = false; + } + go_eat_semi(g); + /* the case body runs until the next case/default or the closing } */ + while (!go_ci(go_cur(g), "case") && !go_ci(go_cur(g), "default") && + go_cur(g)->kind != GO_RBRACE && go_cur(g)->kind != GO_EOF) + { + if (go_ci(go_cur(g), "fallthrough")) + go_err(g, "fallthrough is not supported"); + go_stmt(g, ind + 1); + } + } + if (go_cur(g)->kind == GO_RBRACE) + g->pos++; + go_eat_semi(g); + if (!first) + { + (void) had_default; + plx_indent(&g->cx->out, ind); + appendStringInfoString(&g->cx->out, "END IF;\n"); + } +} + +/* return [expr] */ +static void +go_return(Go *g, int ind) +{ + int e0, + e1; + + g->pos++; /* return */ + e0 = g->pos; + e1 = go_stmt_end(g, e0); + plx_indent(&g->cx->out, ind); + if (e1 > e0) + { + if (g->cx->retset) + appendStringInfoString(&g->cx->out, "RETURN QUERY SELECT"); + else + appendStringInfoString(&g->cx->out, "RETURN"); + go_emit_range(g, e0, e1, &g->cx->out); + appendStringInfoString(&g->cx->out, ";\n"); + } + else + appendStringInfoString(&g->cx->out, "RETURN;\n"); + g->pos = e1; +} + +static void +go_stmt(Go *g, int ind) +{ + GoTok *tk = go_cur(g); + + if (++g->cx->depth > PLX_MAX_DEPTH) + go_err(g, "statement nested too deeply"); + + if (tk->kind == GO_SEMI) + { + g->pos++; + g->cx->depth--; + return; + } + if (tk->kind == GO_LBRACE) /* a bare block: inline its statements */ + { + go_block(g, ind); + go_eat_semi(g); + } + else if (go_ci(tk, "var")) + go_var(g, ind, false); + else if (go_ci(tk, "const")) + go_var(g, ind, true); + else if (go_ci(tk, "if")) + go_if(g, ind); + else if (go_ci(tk, "for")) + go_for(g, ind); + else if (go_ci(tk, "switch")) + go_switch(g, ind); + else if (go_ci(tk, "return")) + go_return(g, ind); + else if (go_ci(tk, "break")) + { + g->pos++; + plx_indent(&g->cx->out, ind); + appendStringInfoString(&g->cx->out, "EXIT;\n"); + go_eat_semi(g); + } + else if (go_ci(tk, "continue")) + { + g->pos++; + plx_indent(&g->cx->out, ind); + appendStringInfoString(&g->cx->out, "CONTINUE;\n"); + go_eat_semi(g); + } + else if (go_ci(tk, "func")) + go_err(g, "nested function definitions are not supported"); + else if (go_ci(tk, "go") || go_ci(tk, "defer") || go_ci(tk, "select") || + go_ci(tk, "goto")) + go_err(g, "go, defer, select, and goto are not supported"); + else + { + go_simple(g, ind, -1); + go_eat_semi(g); + } + + g->cx->depth--; +} + +/* a brace-delimited block: { statements } */ +static void +go_block(Go *g, int ind) +{ + if (go_cur(g)->kind != GO_LBRACE) + go_err(g, "expected {"); + g->pos++; + go_eat_semi(g); + while (go_cur(g)->kind != GO_RBRACE && go_cur(g)->kind != GO_EOF) + go_stmt(g, ind); + if (go_cur(g)->kind != GO_RBRACE) + go_err(g, "missing }"); + g->pos++; +} + +void +plx_go_parse_body(Ctx *cx) +{ + Go g; + + memset(&g, 0, sizeof(g)); + g.cx = cx; + go_lex(&g, cx->body); + go_eat_semi(&g); + + /* an outer { ... } wrapping the whole body is unwrapped */ + if (go_cur(&g)->kind == GO_LBRACE) + { + int close = go_match(&g, g.pos, g.nt); + + if (close == g.nt - 2 || (close > 0 && g.t[close + 1].kind == GO_SEMI && + close + 2 >= g.nt - 1)) + { + g.pos++; + while (go_cur(&g)->kind != GO_RBRACE && go_cur(&g)->kind != GO_EOF) + go_stmt(&g, 1); + return; + } + } + while (go_cur(&g)->kind != GO_EOF) + { + if (go_cur(&g)->kind == GO_RBRACE) + go_err(&g, "unexpected }"); + go_stmt(&g, 1); + } +} + +/* ======================================================================== */ + static const PlxSurface go_surface = { .lanname = "plxgo", .block_style = PLX_BLK_GO, @@ -39,6 +1967,7 @@ static const PlxSurface go_surface = { .excs = NULL, .nexcs = 0, .flags = PLX_TRUSTED, + .parse_body = plx_go_parse_body, }; static char * diff --git a/src/plx_dialect_js.c b/src/plx_dialect_js.c index c24080f..c12d78c 100644 --- a/src/plx_dialect_js.c +++ b/src/plx_dialect_js.c @@ -4,7 +4,8 @@ * JavaScript surface: brace-delimited blocks, no variable sigil, let/const/var * declarations, template-literal interpolation with backticks and ${expr}, * // and block comments, for-of over query(), C-style for, try/catch/finally, - * throw. The shared transpiler in plx_transpile.c does the lowering to plpgsql. + * throw. The brace parser is shared with plxphp/plxts in plx_parse_brace.c; the + * dialect-neutral engine (lowering to plpgsql) lives in plx_transpile.c. */ #include "postgres.h" @@ -13,6 +14,7 @@ #include "plx.h" #include "plx_int.h" +#include "plx_engine.h" PG_FUNCTION_INFO_V1(plx_js_validator); PG_FUNCTION_INFO_V1(plx_js_inline_handler); @@ -48,6 +50,7 @@ static const PlxSurface js_surface = { .excs = NULL, .nexcs = 0, .flags = PLX_TRUSTED, + .parse_body = plx_brace_parse_body, }; static char * diff --git a/src/plx_dialect_php.c b/src/plx_dialect_php.c index a94fd3e..65ab86f 100644 --- a/src/plx_dialect_php.c +++ b/src/plx_dialect_php.c @@ -3,7 +3,8 @@ * * PHP surface (brace-delimited blocks, '$' variables, '.' concatenation, * "$var"/"{$expr}" interpolation, //, #, and block comments, try/catch/finally, - * throw). The shared transpiler in plx_transpile.c does the lowering to plpgsql. + * throw). The brace parser is shared with plxjs/plxts in plx_parse_brace.c; the + * dialect-neutral engine (lowering to plpgsql) lives in plx_transpile.c. */ #include "postgres.h" @@ -12,6 +13,7 @@ #include "plx.h" #include "plx_int.h" +#include "plx_engine.h" PG_FUNCTION_INFO_V1(plx_php_validator); PG_FUNCTION_INFO_V1(plx_php_inline_handler); @@ -47,6 +49,7 @@ static const PlxSurface php_surface = { .excs = NULL, .nexcs = 0, .flags = PLX_TRUSTED, + .parse_body = plx_brace_parse_body, }; static char * diff --git a/src/plx_dialect_plsql.c b/src/plx_dialect_plsql.c index 771e27c..70881d3 100644 --- a/src/plx_dialect_plsql.c +++ b/src/plx_dialect_plsql.c @@ -2,8 +2,8 @@ * plx_dialect_plsql.c - the "plxplsql" dialect (Oracle PL/SQL). * * PL/SQL and plpgsql are both Ada-descended, so most PL/SQL is already valid - * plpgsql. The front end in plx_transpile.c (PLX_BLK_PLSQL) is a layout- - * preserving token rewriter that translates the Oracle-specific spellings + * plpgsql. The front end (below) is a layout-preserving token rewriter that + * translates the Oracle-specific spellings * (NUMBER/VARCHAR2/..., DBMS_OUTPUT.PUT_LINE, RAISE_APPLICATION_ERROR, EXECUTE * IMMEDIATE, FROM DUAL, NVL, seq.NEXTVAL, SYSDATE) and emits the body directly, * since PL/SQL already carries its own DECLARE/BEGIN/END structure. @@ -11,14 +11,423 @@ #include "postgres.h" #include "fmgr.h" +#include "lib/stringinfo.h" +#include "miscadmin.h" #include "utils/memutils.h" #include "plx.h" #include "plx_int.h" +#include "plx_engine.h" PG_FUNCTION_INFO_V1(plx_plsql_validator); PG_FUNCTION_INFO_V1(plx_plsql_inline_handler); +/* ======================================================================== */ +/* PL/SQL front end (plxplsql, Oracle). */ +/* */ +/* PL/SQL and plpgsql are both Ada-descended, so most of the language */ +/* (DECLARE/BEGIN/EXCEPTION/END, IF/ELSIF, LOOP, CASE, assignment (:=), ||, */ +/* cursors, %TYPE) is already valid plpgsql and passes through verbatim. */ +/* This is a token rewriter that preserves the source layout and applies a */ +/* small set of Oracle->Postgres translations: type names (NUMBER, */ +/* VARCHAR2, ...), DBMS_OUTPUT.PUT_LINE, RAISE_APPLICATION_ERROR, EXECUTE */ +/* IMMEDIATE, FROM DUAL, NVL, seq.NEXTVAL, SYSDATE. The body already carries */ +/* its own DECLARE/BEGIN/END, so plxplsql skips the plx wrapper (assemble). */ +/* ======================================================================== */ + +typedef enum +{ + PL_EOF, PL_WORD, PL_NUM, PL_STR, PL_PUNCT +} PlKind; + +typedef struct +{ + PlKind kind; + const char *s; + int len; + const char *pre; /* verbatim whitespace + comments before token */ + int prelen; +} PlTok; + +typedef struct +{ + Ctx *cx; + PlTok *t; + int nt; +} Pl; + +static bool +pl_ci(PlTok *tk, const char *w) +{ + int l = (int) strlen(w); + + return tk->kind == PL_WORD && tk->len == l && pg_strncasecmp(tk->s, w, l) == 0; +} + +static bool +pl_punct(PlTok *tk, char c) +{ + return tk->kind == PL_PUNCT && tk->len == 1 && tk->s[0] == c; +} + +static bool +pl_word_char(char c) +{ + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '_' || c == '$' || c == '#'; +} + +static void +pl_lex(Pl *pl, const char *body) +{ + const char *p = body, + *end = body + strlen(body); + int cap = 256, + n = 0; + PlTok *t = palloc(sizeof(PlTok) * cap); + +#define PLPUSH(k, st, ln, pr, prl) do { \ + if (n >= cap) { cap *= 2; t = repalloc(t, sizeof(PlTok) * cap); } \ + t[n].kind = (k); t[n].s = (st); t[n].len = (ln); \ + t[n].pre = (pr); t[n].prelen = (prl); n++; \ + } while (0) + + while (p < end) + { + const char *pre = p; + + /* accumulate leading trivia: whitespace and comments */ + for (;;) + { + if (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') + p++; + else if (p[0] == '-' && p + 1 < end && p[1] == '-') + { + while (p < end && *p != '\n') + p++; + } + else if (p[0] == '/' && p + 1 < end && p[1] == '*') + { + p += 2; + while (p + 1 < end && !(p[0] == '*' && p[1] == '/')) + p++; + p += 2; + if (p > end) + p = end; + } + else + break; + } + if (p >= end) + { + /* trailing trivia attaches to EOF */ + PLPUSH(PL_EOF, p, 0, pre, (int) (p - pre)); + break; + } + + { + const char *st = p; + int prelen = (int) (st - pre); + + if (*p == '\'') /* string literal */ + { + p++; + while (p < end) + { + if (*p == '\'') + { + if (p + 1 < end && p[1] == '\'') + { + p += 2; + continue; + } + p++; + break; + } + p++; + } + PLPUSH(PL_STR, st, (int) (p - st), pre, prelen); + } + else if (*p == '"') /* quoted identifier */ + { + p++; + while (p < end && *p != '"') + p++; + if (p < end) + p++; + PLPUSH(PL_WORD, st, (int) (p - st), pre, prelen); + } + else if ((*p >= 'A' && *p <= 'Z') || (*p >= 'a' && *p <= 'z') || *p == '_') + { + while (p < end && pl_word_char(*p)) + p++; + PLPUSH(PL_WORD, st, (int) (p - st), pre, prelen); + } + else if (*p >= '0' && *p <= '9') + { + while (p < end && ((*p >= '0' && *p <= '9') || *p == '.' || *p == 'e' || + *p == 'E' || *p == '+' || *p == '-')) + { + /* stop a '.' that begins '..' (range) or is not part of a number */ + if (*p == '.' && p + 1 < end && p[1] == '.') + break; + if ((*p == '+' || *p == '-') && !(p > st && (p[-1] == 'e' || p[-1] == 'E'))) + break; + p++; + } + PLPUSH(PL_NUM, st, (int) (p - st), pre, prelen); + } + else + { + /* punctuation / operators, multi-char first */ + int l = 1; + + if (p + 1 < end) + { + if ((p[0] == ':' && p[1] == '=') || + (p[0] == '|' && p[1] == '|') || + (p[0] == '<' && (p[1] == '=' || p[1] == '>')) || + (p[0] == '>' && p[1] == '=') || + (p[0] == '!' && p[1] == '=') || + (p[0] == '.' && p[1] == '.')) + l = 2; + } + PLPUSH(PL_PUNCT, st, l, pre, prelen); + p += l; + } + } + } + if (n == 0 || t[n - 1].kind != PL_EOF) + PLPUSH(PL_EOF, p, 0, p, 0); + pl->t = t; + pl->nt = n; +#undef PLPUSH +} + +/* Oracle scalar type -> PostgreSQL type, or NULL if not a mapped type word */ +static const char * +pl_type_map(PlTok *tk) +{ + static const struct { const char *o; const char *pg; } m[] = { + {"NUMBER", "numeric"}, {"VARCHAR2", "varchar"}, {"NVARCHAR2", "varchar"}, + {"PLS_INTEGER", "integer"}, {"BINARY_INTEGER", "integer"}, + {"SIMPLE_INTEGER", "integer"}, {"BINARY_FLOAT", "real"}, + {"BINARY_DOUBLE", "double precision"}, {"CLOB", "text"}, {"NCLOB", "text"}, + {"LONG", "text"}, {"BLOB", "bytea"}, {"RAW", "bytea"}, {"NUMBER", "numeric"}, + }; + int i; + + for (i = 0; i < (int) (sizeof(m) / sizeof(m[0])); i++) + if (pl_ci(tk, m[i].o)) + return m[i].pg; + return NULL; +} + +/* single-word function/value substitution (safe to apply anywhere), or NULL */ +static const char * +pl_word_map(PlTok *tk) +{ + if (pl_ci(tk, "NVL")) + return "coalesce"; + if (pl_ci(tk, "SYSDATE")) + return "LOCALTIMESTAMP"; + if (pl_ci(tk, "SYSTIMESTAMP")) + return "clock_timestamp()"; + return NULL; +} + +/* index of the ')' matching the '(' at index lp, or -1 */ +static int +pl_match_paren(Pl *pl, int lp) +{ + int depth = 0, + i; + + for (i = lp; i < pl->nt; i++) + { + if (pl_punct(&pl->t[i], '(')) + depth++; + else if (pl_punct(&pl->t[i], ')')) + { + depth--; + if (depth == 0) + return i; + } + else if (pl->t[i].kind == PL_EOF) + break; + } + return -1; +} + +/* raw source text spanning tokens [a, b) (text only, not leading trivia of a) */ +static void +pl_emit_raw(Pl *pl, int a, int b, StringInfo out) +{ + if (a >= b) + return; + /* from the start of token a's text to the end of token b-1's text */ + appendBinaryStringInfo(out, pl->t[a].s, + (int) (pl->t[b - 1].s + pl->t[b - 1].len - pl->t[a].s)); +} + +void +plx_plsql_parse_body(Ctx *cx) +{ + Pl pl; + StringInfo out = &cx->out; + int i, + first; + bool need_declare; + bool in_decls = true; /* type names map only before the first BEGIN */ + + memset(&pl, 0, sizeof(pl)); + pl.cx = cx; + pl_lex(&pl, cx->body); + + /* find the first real token; if the body does not already open with BEGIN or + * DECLARE, its leading declarations need a DECLARE keyword */ + for (first = 0; first < pl.nt && pl.t[first].kind == PL_EOF; first++) + /* skip (only EOF means empty) */ ; + need_declare = (pl.nt > 0 && pl.t[0].kind != PL_EOF && + !pl_ci(&pl.t[0], "BEGIN") && !pl_ci(&pl.t[0], "DECLARE")); + if (need_declare) + appendStringInfoString(out, "DECLARE"); + + i = 0; + while (i < pl.nt && pl.t[i].kind != PL_EOF) + { + PlTok *t = &pl.t[i]; + + /* leading trivia is always preserved verbatim */ + if (t->prelen) + appendBinaryStringInfo(out, t->pre, t->prelen); + + if (t->kind == PL_WORD) + { + const char *repl; + + /* leaving the declaration section: stop mapping type names, so a body + * reference like "SELECT number FROM t" is not rewritten */ + if (in_decls && pl_ci(t, "BEGIN")) + in_decls = false; + + /* EXECUTE IMMEDIATE -> EXECUTE */ + if (pl_ci(t, "EXECUTE") && i + 1 < pl.nt && pl_ci(&pl.t[i + 1], "IMMEDIATE")) + { + appendStringInfoString(out, "EXECUTE"); + i += 2; + continue; + } + /* FROM DUAL -> (nothing) */ + if (pl_ci(t, "FROM") && i + 1 < pl.nt && pl_ci(&pl.t[i + 1], "DUAL")) + { + i += 2; + continue; + } + /* CURSOR name IS -> name CURSOR FOR */ + if (pl_ci(t, "CURSOR") && i + 2 < pl.nt && pl.t[i + 1].kind == PL_WORD && + pl_ci(&pl.t[i + 2], "IS")) + { + appendBinaryStringInfo(out, pl.t[i + 1].s, pl.t[i + 1].len); + appendStringInfoString(out, " CURSOR FOR"); + i += 3; + continue; + } + /* DBMS_OUTPUT.PUT_LINE(x) / .PUT(x) -> RAISE NOTICE '%', (x) */ + if (pl_ci(t, "DBMS_OUTPUT") && i + 3 < pl.nt && pl_punct(&pl.t[i + 1], '.') && + (pl_ci(&pl.t[i + 2], "PUT_LINE") || pl_ci(&pl.t[i + 2], "PUT")) && + pl_punct(&pl.t[i + 3], '(')) + { + int close = pl_match_paren(&pl, i + 3); + + if (close > 0) + { + appendStringInfoString(out, "RAISE NOTICE '%', ("); + pl_emit_raw(&pl, i + 4, close, out); + appendStringInfoChar(out, ')'); + i = close + 1; + continue; + } + } + /* RAISE_APPLICATION_ERROR(num, msg) -> RAISE EXCEPTION '%', (msg) */ + if (pl_ci(t, "RAISE_APPLICATION_ERROR") && i + 1 < pl.nt && + pl_punct(&pl.t[i + 1], '(')) + { + int close = pl_match_paren(&pl, i + 1); + int comma = -1, + depth = 0, + k; + + for (k = i + 2; close > 0 && k < close; k++) + { + if (pl_punct(&pl.t[k], '(')) + depth++; + else if (pl_punct(&pl.t[k], ')')) + depth--; + else if (depth == 0 && pl_punct(&pl.t[k], ',')) + { + comma = k; + break; + } + } + if (close > 0 && comma > 0) + { + int mend = close; + int d2 = 0, + k2; + + /* message runs to the next top-level comma or the ')' */ + for (k2 = comma + 1; k2 < close; k2++) + { + if (pl_punct(&pl.t[k2], '(')) + d2++; + else if (pl_punct(&pl.t[k2], ')')) + d2--; + else if (d2 == 0 && pl_punct(&pl.t[k2], ',')) + { + mend = k2; + break; + } + } + appendStringInfoString(out, "RAISE EXCEPTION '%', ("); + pl_emit_raw(&pl, comma + 1, mend, out); + appendStringInfoChar(out, ')'); + i = close + 1; + continue; + } + } + /* seq.NEXTVAL / seq.CURRVAL -> nextval('seq') / currval('seq') */ + if (i + 2 < pl.nt && pl_punct(&pl.t[i + 1], '.') && + (pl_ci(&pl.t[i + 2], "NEXTVAL") || pl_ci(&pl.t[i + 2], "CURRVAL"))) + { + appendStringInfo(out, "%s('%.*s')", + pl_ci(&pl.t[i + 2], "NEXTVAL") ? "nextval" : "currval", + t->len, t->s); + i += 3; + continue; + } + /* type name (declaration section only), then function/value maps */ + repl = in_decls ? pl_type_map(t) : NULL; + if (!repl) + repl = pl_word_map(t); + if (repl) + appendStringInfoString(out, repl); + else + appendBinaryStringInfo(out, t->s, t->len); + i++; + continue; + } + + /* strings, numbers, punctuation: verbatim */ + appendBinaryStringInfo(out, t->s, t->len); + i++; + } + + /* trailing trivia (attached to EOF) */ + if (pl.nt > 0 && pl.t[pl.nt - 1].kind == PL_EOF && pl.t[pl.nt - 1].prelen) + appendBinaryStringInfo(out, pl.t[pl.nt - 1].pre, pl.t[pl.nt - 1].prelen); +} + static const PlxSurface plsql_surface = { .lanname = "plxplsql", .block_style = PLX_BLK_PLSQL, @@ -39,6 +448,8 @@ static const PlxSurface plsql_surface = { .excs = NULL, .nexcs = 0, .flags = PLX_TRUSTED, + .parse_body = plx_plsql_parse_body, + .self_contained_block = true, }; static char * diff --git a/src/plx_dialect_python.c b/src/plx_dialect_python.c index 1eaf76b..a0d07fe 100644 --- a/src/plx_dialect_python.c +++ b/src/plx_dialect_python.c @@ -3,16 +3,20 @@ * * Python surface: indentation-based blocks (INDENT/DEDENT), no variable sigil, * f-string interpolation with {expr}, # line comments, if/elif/else, while, - * for-in over range()/query()/array, try/except/finally, raise. The shared - * transpiler in plx_transpile.c does the lowering to plpgsql. + * for-in over range()/query()/array, try/except/finally, raise. The indent + * parser is in this file; the dialect-neutral engine (lowering to plpgsql) + * lives in plx_transpile.c. */ #include "postgres.h" #include "fmgr.h" +#include "lib/stringinfo.h" +#include "miscadmin.h" #include "utils/memutils.h" #include "plx.h" #include "plx_int.h" +#include "plx_engine.h" PG_FUNCTION_INFO_V1(plx_py_validator); PG_FUNCTION_INFO_V1(plx_py_inline_handler); @@ -29,6 +33,547 @@ static const PlxKwSpell py_kws[] = { {"emit", KW_EMIT}, {"return_next", KW_RETURN_NEXT}, }; +/* ---- Python (indent) front end. Parser + recursion guard + parse_body. ---- */ +static void parse_py_stmt(Ctx *cx, int ind, bool toplevel); +static void parse_py_stmt_inner(Ctx *cx, int ind, bool toplevel); +static void parse_py_block(Ctx *cx, int ind); +static void parse_py_program(Ctx *cx); + +static void +parse_py_stmt(Ctx *cx, int ind, bool toplevel) +{ + check_stack_depth(); + if (++cx->depth > PLX_MAX_DEPTH) + plx_err(cx, cx->t[cx->pos].line, "statements nested too deeply"); + parse_py_stmt_inner(cx, ind, toplevel); + cx->depth--; +} + +/* ---------------------------------------------------------------- python (indent) parser */ + +/* index of the ':' that ends a compound-statement header (bracket depth 0) */ +static int +py_header_colon(Ctx *cx, int from) +{ + int i = from, depth = 0; + + while (cx->t[i].kind != T_EOF && cx->t[i].kind != T_NEWLINE) + { + TokKind k = cx->t[i].kind; + + if (k == T_LPAREN || k == T_LBRACKET || k == T_LBRACE) + depth++; + else if (k == T_RPAREN || k == T_RBRACKET || k == T_RBRACE) + depth--; + else if (depth == 0 && plx_tok_is(&cx->t[i], ":")) + return i; + i++; + } + return -1; +} + +/* consume NEWLINE INDENT DEDENT */ +static void +parse_py_block(Ctx *cx, int ind) +{ + plx_skip_seps(cx); + if (cx->t[cx->pos].kind != T_INDENT) + plx_err(cx, cx->t[cx->pos].line, "expected an indented block"); + cx->pos++; + for (;;) + { + plx_skip_seps(cx); + if (cx->t[cx->pos].kind == T_DEDENT) + { + cx->pos++; + return; + } + if (cx->t[cx->pos].kind == T_EOF) + return; + parse_py_stmt(cx, ind, false); + } +} + +static void +parse_py_if(Ctx *cx, int ind) +{ + StringInfo o = &cx->out; + int hd = cx->t[cx->pos].line; + int colon; + + colon = py_header_colon(cx, cx->pos + 1); + if (colon < 0) + plx_err(cx, hd, "if header requires ':'"); + plx_indent(o, ind); + appendStringInfo(o, "IF %s THEN\n", plx_rw_range(cx, cx->pos + 1, colon, true)); + cx->pos = colon + 1; + parse_py_block(cx, ind + 1); + while (cx->t[cx->pos].kind == T_KW && cx->t[cx->pos].kw == KW_ELSIF) /* elif */ + { + colon = py_header_colon(cx, cx->pos + 1); + if (colon < 0) + plx_err(cx, cx->t[cx->pos].line, "elif header requires ':'"); + plx_indent(o, ind); + appendStringInfo(o, "ELSIF %s THEN\n", plx_rw_range(cx, cx->pos + 1, colon, true)); + cx->pos = colon + 1; + parse_py_block(cx, ind + 1); + } + if (cx->t[cx->pos].kind == T_KW && cx->t[cx->pos].kw == KW_ELSE) + { + colon = py_header_colon(cx, cx->pos + 1); + if (colon < 0) + plx_err(cx, cx->t[cx->pos].line, "else header requires ':'"); + plx_indent(o, ind); + appendStringInfoString(o, "ELSE\n"); + cx->pos = colon + 1; + parse_py_block(cx, ind + 1); + } + plx_indent(o, ind); + appendStringInfoString(o, "END IF;\n"); +} + +static void +parse_py_while(Ctx *cx, int ind) +{ + StringInfo o = &cx->out; + int hd = cx->t[cx->pos].line; + int colon = py_header_colon(cx, cx->pos + 1); + + if (colon < 0) + plx_err(cx, hd, "while header requires ':'"); + plx_indent(o, ind); + appendStringInfo(o, "WHILE %s LOOP\n", plx_rw_range(cx, cx->pos + 1, colon, true)); + cx->pos = colon + 1; + cx->loopdepth++; + parse_py_block(cx, ind + 1); + cx->loopdepth--; + plx_indent(o, ind); + appendStringInfoString(o, "END LOOP;\n"); +} + +/* for VAR in : */ +static void +parse_py_for(Ctx *cx, int ind) +{ + StringInfo o = &cx->out; + int hd = cx->t[cx->pos].line; + int colon = py_header_colon(cx, cx->pos + 1); + int var, in_at, ia, ib; + + if (colon < 0) + plx_err(cx, hd, "for header requires ':'"); + var = cx->pos + 1; + if (cx->t[var].kind != T_IDENT) + plx_err(cx, hd, "for requires a loop variable"); + in_at = var + 1; + if (!(cx->t[in_at].kind == T_KW && cx->t[in_at].kw == KW_IN)) + plx_err(cx, hd, "for requires 'in'"); + ia = in_at + 1; + ib = colon; + + if (cx->t[ia].kind == T_IDENT && plx_name_eq(&cx->t[ia], "range") && + cx->t[ia + 1].kind == T_LPAREN) + { + int as[8], ae[8], after, n; + char *lo, *hi, *step = NULL; + + n = plx_parse_args(cx, ia, as, ae, 8, &after); + if (n == 1) + { + lo = pstrdup("0"); + hi = plx_rw_range(cx, as[0], ae[0], false); + } + else if (n >= 2) + { + lo = plx_rw_range(cx, as[0], ae[0], false); + hi = plx_rw_range(cx, as[1], ae[1], false); + if (n >= 3) + step = plx_rw_range(cx, as[2], ae[2], false); + } + else + plx_err(cx, hd, "range requires 1 to 3 arguments"); + plx_indent(o, ind); + appendStringInfo(o, "FOR %.*s IN %s..(%s - 1)%s%s LOOP\n", + cx->t[var].len, cx->t[var].s, lo, hi, + step ? " BY " : "", step ? step : ""); + cx->pos = colon + 1; + cx->loopdepth++; + parse_py_block(cx, ind + 1); + cx->loopdepth--; + } + else if (cx->t[ia].kind == T_IDENT && plx_name_eq(&cx->t[ia], "query") && + cx->t[ia + 1].kind == T_LPAREN) + { + int as[16], ae[16], after, n; + + n = plx_parse_args(cx, ia, as, ae, 16, &after); + if (!plx_is_param(cx, cx->t[var].s, cx->t[var].len)) + { + PlxLocal2 *l = plx_local_find(cx, cx->t[var].s, cx->t[var].len); + + if (!l) + l = plx_local_add(cx, cx->t[var].s, cx->t[var].len); + l->is_record = true; + } + plx_indent(o, ind); + if (n == 1 && plx_arg_is_string_literal(cx, as[0], ae[0])) + { + appendStringInfo(o, "FOR %.*s IN ", cx->t[var].len, cx->t[var].s); + plx_emit_string_as_sql(cx, &cx->t[as[0]], o); + appendStringInfoString(o, " LOOP\n"); + } + else + { + char *sqlv = plx_rw_range(cx, as[0], ae[0], false); + char *binds = plx_binds_text(cx, as, ae, n); + + if (binds) + appendStringInfo(o, "FOR %.*s IN EXECUTE %s USING %s LOOP\n", + cx->t[var].len, cx->t[var].s, sqlv, binds); + else + appendStringInfo(o, "FOR %.*s IN EXECUTE %s LOOP\n", + cx->t[var].len, cx->t[var].s, sqlv); + } + cx->pos = colon + 1; + cx->loopdepth++; + parse_py_block(cx, ind + 1); + cx->loopdepth--; + } + else + { + /* FOREACH over an array */ + char *arrx = plx_rw_range(cx, ia, ib, false); + PlxLocal2 *lv; + + if (plx_is_param(cx, cx->t[var].s, cx->t[var].len)) + plx_err(cx, hd, "foreach-array loop variable must be a local, not a parameter"); + lv = plx_local_find(cx, cx->t[var].s, cx->t[var].len); + if (!lv || !lv->typ) + plx_err(cx, hd, "iterating an array requires the loop variable to be annotated with its element type before the loop"); + plx_indent(o, ind); + appendStringInfo(o, "FOREACH %.*s IN ARRAY %s%s LOOP\n", + cx->t[var].len, cx->t[var].s, arrx[0] == '[' ? "ARRAY" : "", arrx); + cx->pos = colon + 1; + cx->loopdepth++; + parse_py_block(cx, ind + 1); + cx->loopdepth--; + } + plx_indent(o, ind); + appendStringInfoString(o, "END LOOP;\n"); +} + +/* try: except [Class as e]: ... [finally: ] */ +static void +parse_py_try(Ctx *cx, int ind) +{ + StringInfo o = &cx->out; + int hd = cx->t[cx->pos].line; + int colon; + char *body; + StringInfoData saved, + arms; + int nrescue = 0; + char *ensure_body = NULL; + + colon = py_header_colon(cx, cx->pos + 1); + if (colon < 0) + plx_err(cx, hd, "try requires ':'"); + cx->pos = colon + 1; + saved = cx->out; + initStringInfo(&cx->out); + parse_py_block(cx, ind + 1); + body = cx->out.data; + cx->out = saved; + + initStringInfo(&arms); + while (cx->t[cx->pos].kind == T_KW && cx->t[cx->pos].kw == KW_RESCUE) /* except */ + { + int es = cx->pos; + const char *cond = "OTHERS"; + int ev_a = -1, + as_at = -1, + i; + const char *save_ev = cx->exc_var; + int save_evl = cx->exc_varlen; + int save_diag; + char *hb, + *pfx; + + colon = py_header_colon(cx, es + 1); + if (colon < 0) + plx_err(cx, cx->t[es].line, "except requires ':'"); + /* except Class as e: or except Class: or except: */ + for (i = es + 1; i < colon; i++) + if (cx->t[i].kind == T_KW && cx->t[i].kw == KW_AS) + { + as_at = i; + break; + } + if (as_at >= 0 && as_at + 1 < colon && cx->t[as_at + 1].kind == T_IDENT) + ev_a = as_at + 1; + { + int cls_b = (as_at >= 0) ? as_at : colon; + + if (cls_b > es + 1) + { + char *cls = plx_span_text(cx, es + 1, cls_b); + const char *c = plx_exc_class_to_condition(cls, (int) strlen(cls)); + + if (c) + cond = c; + } + } + cx->pos = colon + 1; + if (ev_a >= 0) + { + cx->exc_var = cx->t[ev_a].s; + cx->exc_varlen = cx->t[ev_a].len; + } + save_diag = cx->diag_mask; + cx->diag_mask = 0; + cx->handlerdepth++; + saved = cx->out; + initStringInfo(&cx->out); + parse_py_block(cx, ind + 2); + hb = cx->out.data; + cx->out = saved; + cx->handlerdepth--; + pfx = plx_diag_prefix(cx->diag_mask, ind + 2); + cx->diag_mask = save_diag; + cx->exc_var = save_ev; + cx->exc_varlen = save_evl; + + appendStringInfoSpaces(&arms, (ind + 1) * 2); + appendStringInfo(&arms, "WHEN %s THEN\n", cond); + appendStringInfoString(&arms, pfx); + appendStringInfoString(&arms, hb); + nrescue++; + } + if (cx->t[cx->pos].kind == T_KW && cx->t[cx->pos].kw == KW_ENSURE) /* finally */ + { + colon = py_header_colon(cx, cx->pos + 1); + if (colon < 0) + plx_err(cx, cx->t[cx->pos].line, "finally requires ':'"); + cx->pos = colon + 1; + saved = cx->out; + initStringInfo(&cx->out); + parse_py_block(cx, ind + 1); + ensure_body = cx->out.data; + cx->out = saved; + } + + if (!ensure_body) + { + plx_indent(o, ind); + appendStringInfoString(o, "BEGIN\n"); + appendStringInfoString(o, body); + if (nrescue) + { + plx_indent(o, ind); + appendStringInfoString(o, "EXCEPTION\n"); + appendStringInfoString(o, arms.data); + } + plx_indent(o, ind); + appendStringInfoString(o, "END;\n"); + } + else + { + plx_indent(o, ind); + appendStringInfoString(o, "BEGIN\n"); + plx_indent(o, ind + 1); + appendStringInfoString(o, "BEGIN\n"); + appendStringInfoString(o, body); + if (nrescue) + { + plx_indent(o, ind + 1); + appendStringInfoString(o, "EXCEPTION\n"); + appendStringInfoString(o, arms.data); + } + plx_indent(o, ind + 1); + appendStringInfoString(o, "END;\n"); + appendStringInfoString(o, ensure_body); + plx_indent(o, ind); + appendStringInfoString(o, "EXCEPTION WHEN OTHERS THEN\n"); + appendStringInfoString(o, ensure_body); + plx_indent(o, ind + 1); + appendStringInfoString(o, "RAISE;\n"); + plx_indent(o, ind); + appendStringInfoString(o, "END;\n"); + } +} + +/* raise / raise Class("msg") */ +static void +emit_py_raise(Ctx *cx, int a, int b, int ind) +{ + int call = -1, i; + + if (a + 1 >= b) /* bare raise (re-raise) */ + { + if (cx->handlerdepth == 0) + plx_err(cx, cx->t[a].line, "bare 'raise' is only valid inside an except handler"); + plx_indent(&cx->out, ind); + appendStringInfoString(&cx->out, "RAISE;\n"); + return; + } + /* raise(msg) / raise("level", msg): the call form emits a leveled RAISE */ + if (cx->t[a + 1].kind == T_LPAREN) + { + plx_emit_raise_call(cx, a, ind); + return; + } + for (i = a + 1; i < b; i++) + if (cx->t[i].kind == T_LPAREN) + { + call = i - 1; + break; + } + if (call < 0) + plx_err(cx, cx->t[a].line, "raise requires an exception, e.g. raise ValueError(\"msg\")"); + { + int sa[8], se[8], after, n; + + n = plx_parse_args(cx, call, sa, se, 8, &after); + plx_indent(&cx->out, ind); + if (n >= 1) + appendStringInfo(&cx->out, "RAISE EXCEPTION '%%', %s;\n", + plx_rw_range(cx, sa[0], se[0], false)); + else + appendStringInfoString(&cx->out, "RAISE EXCEPTION 'exception';\n"); + } +} + +static void +parse_py_stmt_inner(Ctx *cx, int ind, bool toplevel) +{ + Tok *tk; + + plx_skip_seps(cx); + tk = &cx->t[cx->pos]; + if (tk->kind == T_EOF || tk->kind == T_DEDENT) + return; + /* loop label: name: for ... / name: while ... */ + if (tk->kind == T_IDENT && plx_tok_is(&cx->t[cx->pos + 1], ":") && + cx->t[cx->pos + 2].kind == T_KW && + (cx->t[cx->pos + 2].kw == KW_FOR || cx->t[cx->pos + 2].kw == KW_WHILE)) + { + plx_indent(&cx->out, ind); + appendStringInfo(&cx->out, "<<%.*s>>\n", tk->len, tk->s); + cx->pos += 2; + tk = &cx->t[cx->pos]; + } + if (tk->kind == T_KW) + { + switch (tk->kw) + { + case KW_IF: + parse_py_if(cx, ind); + return; + case KW_WHILE: + parse_py_while(cx, ind); + return; + case KW_FOR: + parse_py_for(cx, ind); + return; + case KW_BEGIN: /* try */ + parse_py_try(cx, ind); + return; + case KW_PASS: + cx->pos = plx_stmt_end(cx, cx->pos); + return; + case KW_DEF: + plx_err(cx, tk->line, "def is not supported"); + return; + case KW_CASE: + plx_err(cx, tk->line, "match/case is not supported (use if/elif)"); + return; + default: + break; + } + } + /* leaf statement (ends at NEWLINE) */ + { + int a = cx->pos; + int b = plx_stmt_end(cx, a); + int i, depth = 0; + + /* a top-level if/else inside a leaf is a conditional expression + * (a if c else b), which has no plpgsql lowering */ + for (i = a; i < b; i++) + { + TokKind k = cx->t[i].kind; + + if (k == T_LPAREN || k == T_LBRACKET || k == T_LBRACE) + depth++; + else if (k == T_RPAREN || k == T_RBRACKET || k == T_RBRACE) + depth--; + else if (depth == 0 && cx->t[i].kind == T_KW && + (cx->t[i].kw == KW_IF || cx->t[i].kw == KW_ELSE)) + plx_err(cx, cx->t[i].line, + "the conditional expression \"a if c else b\" is not supported; use an if statement"); + } + + if (tk->kind == T_KW && tk->kw == KW_RAISE) + emit_py_raise(cx, a, b, ind); + else if (tk->kind == T_IDENT && plx_name_eq(tk, "assert") && + !(cx->t[a + 1].kind == T_LPAREN)) + { + /* Python statement form: assert cond[, msg] */ + int ai, adepth = 0, comma = -1; + + for (ai = a + 1; ai < b; ai++) + { + if (cx->t[ai].kind == T_LPAREN || cx->t[ai].kind == T_LBRACKET) + adepth++; + else if (cx->t[ai].kind == T_RPAREN || cx->t[ai].kind == T_RBRACKET) + adepth--; + else if (adepth == 0 && cx->t[ai].kind == T_COMMA) + { + comma = ai; + break; + } + } + plx_indent(&cx->out, ind); + if (comma >= 0) + appendStringInfo(&cx->out, "ASSERT %s, %s;\n", + plx_rw_range(cx, a + 1, comma, true), plx_rw_range(cx, comma + 1, b, false)); + else + appendStringInfo(&cx->out, "ASSERT %s;\n", plx_rw_range(cx, a + 1, b, true)); + } + else + plx_emit_core(cx, a, b, ind, toplevel); + cx->pos = b; + } +} + +static void +parse_py_program(Ctx *cx) +{ + for (;;) + { + plx_skip_seps(cx); + if (cx->t[cx->pos].kind == T_EOF) + break; + if (cx->t[cx->pos].kind == T_INDENT || cx->t[cx->pos].kind == T_DEDENT) + { + cx->pos++; /* stray indentation token at top level */ + continue; + } + parse_py_stmt(cx, 1, true); + } +} + + + + +void +plx_python_parse_body(Ctx *cx) +{ + plx_lex(cx); + parse_py_program(cx); +} + + static const PlxSurface py_surface = { .lanname = "plxpython3", .block_style = PLX_BLK_INDENT, @@ -49,6 +594,7 @@ static const PlxSurface py_surface = { .excs = NULL, .nexcs = 0, .flags = PLX_TRUSTED, + .parse_body = plx_python_parse_body, }; static char * diff --git a/src/plx_dialect_ruby.c b/src/plx_dialect_ruby.c index f4d3886..e5431b7 100644 --- a/src/plx_dialect_ruby.c +++ b/src/plx_dialect_ruby.c @@ -8,10 +8,13 @@ #include "postgres.h" #include "fmgr.h" +#include "lib/stringinfo.h" +#include "miscadmin.h" #include "utils/memutils.h" #include "plx.h" #include "plx_int.h" +#include "plx_engine.h" PG_FUNCTION_INFO_V1(plx_ruby_validator); PG_FUNCTION_INFO_V1(plx_ruby_inline_handler); @@ -27,6 +30,760 @@ static const PlxKwSpell ruby_kws[] = { {"nil", KW_NIL}, {"true", KW_TRUE}, {"false", KW_FALSE}, }; +/* ---- Ruby (keyword-end) front end. Parser + recursion guard + parse_body. --- */ +static void parse_stmt(Ctx *cx, int indent, bool toplevel); +static void parse_stmt_inner(Ctx *cx, int ind, bool toplevel); +static void parse_block(Ctx *cx, int ind); +static void parse_iter(Ctx *cx, int a, int do_pos, int e, int ind); +static void parse_begin(Ctx *cx, int ind); +static void parse_case(Ctx *cx, int ind); + +static void +parse_stmt(Ctx *cx, int indent, bool toplevel) +{ + check_stack_depth(); + if (++cx->depth > PLX_MAX_DEPTH) + plx_err(cx, cx->t[cx->pos].line, "statements nested too deeply"); + parse_stmt_inner(cx, indent, toplevel); + cx->depth--; +} + +/* ---------------------------------------------------------------- blocks */ + +static void +parse_if(Ctx *cx, int ind) +{ + StringInfo o = &cx->out; + int start = cx->pos; + Tok *hd = &cx->t[start]; + bool neg = (hd->kw == KW_UNLESS); + int e = plx_stmt_end(cx, start); + int cond_a = start + 1; + int cond_b = e; + char *cond; + + /* optional trailing 'then' */ + if (cond_b > cond_a && cx->t[cond_b - 1].kind == T_KW && cx->t[cond_b - 1].kw == KW_THEN) + cond_b--; + cond = plx_rewrite_expr(cx, plx_span_text(cx, cond_a, cond_b), + (int) strlen(plx_span_text(cx, cond_a, cond_b)), true); + plx_indent(o, ind); + appendStringInfo(o, "IF %s%s%s THEN\n", neg ? "NOT (" : "", cond, neg ? ")" : ""); + cx->pos = e; + parse_block(cx, ind + 1); + /* handle elsif/else chain */ + while (cx->t[cx->pos].kind == T_KW && + (cx->t[cx->pos].kw == KW_ELSIF || cx->t[cx->pos].kw == KW_ELSE)) + { + if (cx->t[cx->pos].kw == KW_ELSIF) + { + int es = cx->pos; + int ee = plx_stmt_end(cx, es); + int ca = es + 1, cb = ee; + char *ec; + + if (cb > ca && cx->t[cb - 1].kind == T_KW && cx->t[cb - 1].kw == KW_THEN) + cb--; + ec = plx_rewrite_expr(cx, plx_span_text(cx, ca, cb), + (int) strlen(plx_span_text(cx, ca, cb)), true); + plx_indent(o, ind); + appendStringInfo(o, "ELSIF %s THEN\n", ec); + cx->pos = ee; + parse_block(cx, ind + 1); + } + else + { + plx_indent(o, ind); + appendStringInfoString(o, "ELSE\n"); + cx->pos++; /* consume else */ + parse_block(cx, ind + 1); + break; + } + } + if (cx->t[cx->pos].kind == T_KW && cx->t[cx->pos].kw == KW_END) + cx->pos++; + else + plx_err(cx, hd->line, "unterminated if/unless block"); + plx_indent(o, ind); + appendStringInfoString(o, "END IF;\n"); +} + +static void +parse_while(Ctx *cx, int ind) +{ + StringInfo o = &cx->out; + int start = cx->pos; + Tok *hd = &cx->t[start]; + bool neg = (hd->kw == KW_UNTIL); + int e = plx_stmt_end(cx, start); + int ca = start + 1, cb = e; + char *cond; + + if (cb > ca && cx->t[cb - 1].kind == T_KW && cx->t[cb - 1].kw == KW_DO) + cb--; + cond = plx_rewrite_expr(cx, plx_span_text(cx, ca, cb), + (int) strlen(plx_span_text(cx, ca, cb)), true); + plx_indent(o, ind); + appendStringInfo(o, "WHILE %s%s%s LOOP\n", neg ? "NOT (" : "", cond, neg ? ")" : ""); + cx->pos = e; + cx->loopdepth++; + parse_block(cx, ind + 1); + cx->loopdepth--; + if (cx->t[cx->pos].kind == T_KW && cx->t[cx->pos].kw == KW_END) + cx->pos++; + else + plx_err(cx, hd->line, "unterminated while/until block"); + plx_indent(o, ind); + appendStringInfoString(o, "END LOOP;\n"); +} + +static void +parse_loop(Ctx *cx, int ind) +{ + StringInfo o = &cx->out; + int start = cx->pos; + int e = plx_stmt_end(cx, start); + + plx_indent(o, ind); + appendStringInfoString(o, "LOOP\n"); + cx->pos = e; + cx->loopdepth++; + parse_block(cx, ind + 1); + cx->loopdepth--; + if (cx->t[cx->pos].kind == T_KW && cx->t[cx->pos].kw == KW_END) + cx->pos++; + else + plx_err(cx, cx->t[start].line, "unterminated loop block"); + plx_indent(o, ind); + appendStringInfoString(o, "END LOOP;\n"); +} + +/* integer for: for V in LO..HI | for V in LO...HI */ +static void +parse_for(Ctx *cx, int ind) +{ + StringInfo o = &cx->out; + int start = cx->pos; + Tok *hd = &cx->t[start]; + int e = plx_stmt_end(cx, start); + int p = start + 1; + int var_a, var_b; + int in_at = -1, range_at = -1; + bool exclusive = false; + int i; + char *lo, *hi; + int hi_a, hi_b; + + if (cx->t[p].kind != T_IDENT) + plx_err(cx, hd->line, "for loop expects a variable name"); + var_a = p; + var_b = p + 1; + if (!(cx->t[p + 1].kind == T_KW && cx->t[p + 1].kw == KW_IN)) + plx_err(cx, hd->line, "for loop expects 'in'"); + in_at = p + 1; + /* find the range operator .. or ... at top level */ + for (i = in_at + 1; i < e; i++) + { + if (plx_tok_is(&cx->t[i], "..") || (cx->t[i].kind == T_OP && cx->t[i].len >= 2 && + strncmp(cx->t[i].s, "..", 2) == 0)) + { + range_at = i; + exclusive = (cx->t[i].len == 3); /* ... */ + break; + } + /* a lone DOT DOT sequence (lexer may split) */ + } + if (range_at < 0) + plx_err(cx, hd->line, "for loop requires an integer range LO..HI (only integer ranges supported in M3a)"); + /* trailing 'do' */ + if (e > in_at && cx->t[e - 1].kind == T_KW && cx->t[e - 1].kw == KW_DO) + e--; + lo = plx_rewrite_expr(cx, plx_span_text(cx, in_at + 1, range_at), + (int) strlen(plx_span_text(cx, in_at + 1, range_at)), false); + hi_a = range_at + 1; + hi_b = e; + hi = plx_rewrite_expr(cx, plx_span_text(cx, hi_a, hi_b), + (int) strlen(plx_span_text(cx, hi_a, hi_b)), false); + plx_indent(o, ind); + if (exclusive) + appendStringInfo(o, "FOR %.*s IN %s..(%s - 1) LOOP\n", + cx->t[var_a].len, cx->t[var_a].s, lo, hi); + else + appendStringInfo(o, "FOR %.*s IN %s..%s LOOP\n", + cx->t[var_a].len, cx->t[var_a].s, lo, hi); + (void) var_b; + cx->pos = e; + /* skip the 'do' token if we trimmed it */ + if (cx->t[cx->pos].kind == T_KW && cx->t[cx->pos].kw == KW_DO) + cx->pos++; + cx->loopdepth++; + parse_block(cx, ind + 1); + cx->loopdepth--; + if (cx->t[cx->pos].kind == T_KW && cx->t[cx->pos].kw == KW_END) + cx->pos++; + else + plx_err(cx, hd->line, "unterminated for block"); + plx_indent(o, ind); + appendStringInfoString(o, "END LOOP;\n"); +} + +/* capture a parse_block into a string (temporarily swaps cx->out) */ +static char * +capture_block(Ctx *cx, int ind) +{ + StringInfoData saved = cx->out; + char *r; + + initStringInfo(&cx->out); + parse_block(cx, ind); + r = cx->out.data; + cx->out = saved; + return r; +} + +/* iterator block: query(...).each do |row| ... end | (LO..HI).each do |v| ... end */ +static void +parse_iter(Ctx *cx, int a, int do_pos, int e, int ind) +{ + StringInfo o = &cx->out; + int each_at = -1; + bool with_index = false; + int recv_end; + int vp = do_pos + 1; + int var1_a = -1, var2_a = -1; + int i; + + for (i = a + 1; i < do_pos; i++) + if (cx->t[i].kind == T_IDENT && + (plx_name_eq(&cx->t[i], "each") || plx_name_eq(&cx->t[i], "each_with_index")) && + cx->t[i - 1].kind == T_DOT) + { + each_at = i; + with_index = plx_name_eq(&cx->t[i], "each_with_index"); + } + if (each_at < 0) + plx_err(cx, cx->t[a].line, "unsupported iterator (use query(...).each or (LO..HI).each)"); + recv_end = each_at - 1; + + if (cx->t[vp].kind != T_PIPE || cx->t[vp + 1].kind != T_IDENT) + plx_err(cx, cx->t[a].line, "iterator block requires |var|"); + var1_a = vp + 1; + vp += 2; + if (cx->t[vp].kind == T_COMMA) + { + if (cx->t[vp + 1].kind != T_IDENT) + plx_err(cx, cx->t[a].line, "iterator block index requires a name"); + var2_a = vp + 1; + vp += 2; + } + if (cx->t[vp].kind != T_PIPE) + plx_err(cx, cx->t[a].line, "iterator block variable list not closed with |"); + + if (cx->t[a].kind == T_IDENT && plx_name_eq(&cx->t[a], "query") && + cx->t[a + 1].kind == T_LPAREN) + { + int as[16], ae[16], after, n; + + n = plx_parse_args(cx, a, as, ae, 16, &after); + if (n < 1) + plx_err(cx, cx->t[a].line, "query requires a SQL argument"); + if (!plx_is_param(cx, cx->t[var1_a].s, cx->t[var1_a].len)) + { + PlxLocal2 *l = plx_local_find(cx, cx->t[var1_a].s, cx->t[var1_a].len); + + if (!l) + l = plx_local_add(cx, cx->t[var1_a].s, cx->t[var1_a].len); + l->is_record = true; + } + if (with_index) + { + PlxLocal2 *li; + + if (var2_a < 0) + plx_err(cx, cx->t[a].line, "each_with_index requires |row, idx|"); + li = plx_local_find(cx, cx->t[var2_a].s, cx->t[var2_a].len); + if (!li) + li = plx_local_add(cx, cx->t[var2_a].s, cx->t[var2_a].len); + if (!li->typ) + li->typ = pstrdup("integer"); + plx_indent(o, ind); + appendStringInfo(o, "%.*s := -1;\n", cx->t[var2_a].len, cx->t[var2_a].s); + } + plx_indent(o, ind); + if (n == 1 && plx_arg_is_string_literal(cx, as[0], ae[0])) + { + appendStringInfo(o, "FOR %.*s IN ", cx->t[var1_a].len, cx->t[var1_a].s); + plx_emit_string_as_sql(cx, &cx->t[as[0]], o); + appendStringInfoString(o, " LOOP\n"); + } + else + { + char *st = plx_span_text(cx, as[0], ae[0]); + char *sqlv = plx_rewrite_expr(cx, st, (int) strlen(st), false); + char *binds = plx_binds_text(cx, as, ae, n); + + if (binds) + appendStringInfo(o, "FOR %.*s IN EXECUTE %s USING %s LOOP\n", + cx->t[var1_a].len, cx->t[var1_a].s, sqlv, binds); + else + appendStringInfo(o, "FOR %.*s IN EXECUTE %s LOOP\n", + cx->t[var1_a].len, cx->t[var1_a].s, sqlv); + } + cx->pos = e; + cx->loopdepth++; + if (with_index) + { + plx_indent(o, ind + 1); + appendStringInfo(o, "%.*s := %.*s + 1;\n", + cx->t[var2_a].len, cx->t[var2_a].s, + cx->t[var2_a].len, cx->t[var2_a].s); + } + parse_block(cx, ind + 1); + cx->loopdepth--; + } + else if (cx->t[a].kind == T_LPAREN) + { + int rng = -1; + bool excl = false; + int lo_a = a + 1, lo_b, hi_a, hi_b = recv_end - 1; + char *lo, *hi, *lst, *hst; + + for (i = a + 1; i < recv_end; i++) + if (cx->t[i].kind == T_OP && cx->t[i].len >= 2 && + strncmp(cx->t[i].s, "..", 2) == 0) + { + rng = i; + excl = (cx->t[i].len == 3); + break; + } + if (rng < 0) + plx_err(cx, cx->t[a].line, "range iterator requires LO..HI"); + lo_b = rng; + hi_a = rng + 1; + lst = plx_span_text(cx, lo_a, lo_b); + hst = plx_span_text(cx, hi_a, hi_b); + lo = plx_rewrite_expr(cx, lst, (int) strlen(lst), false); + hi = plx_rewrite_expr(cx, hst, (int) strlen(hst), false); + plx_indent(o, ind); + if (excl) + appendStringInfo(o, "FOR %.*s IN %s..(%s - 1) LOOP\n", + cx->t[var1_a].len, cx->t[var1_a].s, lo, hi); + else + appendStringInfo(o, "FOR %.*s IN %s..%s LOOP\n", + cx->t[var1_a].len, cx->t[var1_a].s, lo, hi); + cx->pos = e; + cx->loopdepth++; + parse_block(cx, ind + 1); + cx->loopdepth--; + } + else + { + /* FOREACH over an array expression: arr.each do |v| ... end */ + char *arr = plx_span_text(cx, a, recv_end); + char *arrx = plx_rewrite_expr(cx, arr, (int) strlen(arr), false); + PlxLocal2 *lv; + + if (plx_is_param(cx, cx->t[var1_a].s, cx->t[var1_a].len)) + plx_err(cx, cx->t[a].line, "foreach-array loop variable must be a local, not a parameter"); + lv = plx_local_find(cx, cx->t[var1_a].s, cx->t[var1_a].len); + if (!lv || !lv->typ) + plx_err(cx, cx->t[a].line, + "foreach over an array requires the loop variable to be annotated with its element type before the loop, e.g. \"%.*s #:: int\"", + cx->t[var1_a].len, cx->t[var1_a].s); + plx_indent(o, ind); + appendStringInfo(o, "FOREACH %.*s IN ARRAY %s%s LOOP\n", + cx->t[var1_a].len, cx->t[var1_a].s, + arrx[0] == '[' ? "ARRAY" : "", arrx); + cx->pos = e; + cx->loopdepth++; + parse_block(cx, ind + 1); + cx->loopdepth--; + } + + plx_skip_seps(cx); + if (cx->t[cx->pos].kind == T_KW && cx->t[cx->pos].kw == KW_END) + cx->pos++; + else + plx_err(cx, cx->t[a].line, "unterminated iterator block"); + plx_indent(o, 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) +{ + StringInfo o = &cx->out; + int bl = cx->t[cx->pos].line; + char *body; + StringInfoData arms; + int nrescue = 0; + char *ensure_body = NULL; + + cx->pos++; /* consume 'begin' */ + body = capture_block(cx, ind + 1); + + initStringInfo(&arms); + while (cx->t[cx->pos].kind == T_KW && cx->t[cx->pos].kw == KW_RESCUE) + { + int rs = cx->pos; + int re = plx_stmt_end(cx, rs); + const char *cond = "OTHERS"; + int arrow = -1, i; + int ev_a = -1; + const char *save_ev = cx->exc_var; + int save_evl = cx->exc_varlen; + int cls_a, cls_b; + char *hb; + + for (i = rs + 1; i < re; i++) + if (plx_tok_is(&cx->t[i], "=>")) + { + arrow = i; + break; + } + if (arrow >= 0 && arrow + 1 < re && cx->t[arrow + 1].kind == T_IDENT) + ev_a = arrow + 1; + cls_a = rs + 1; + cls_b = (arrow >= 0) ? arrow : re; + if (cls_b > cls_a) + { + char *clstext = plx_span_text(cx, cls_a, cls_b); + const char *c = plx_exc_class_to_condition(clstext, (int) strlen(clstext)); + + if (!c) + plx_err(cx, cx->t[rs].line, "unsupported exception class \"%s\"", clstext); + cond = c; + } + cx->pos = re; + if (ev_a >= 0) + { + cx->exc_var = cx->t[ev_a].s; + cx->exc_varlen = cx->t[ev_a].len; + } + { + int save_diag = cx->diag_mask; + char *pfx; + + cx->diag_mask = 0; + cx->handlerdepth++; + hb = capture_block(cx, ind + 2); + cx->handlerdepth--; + pfx = plx_diag_prefix(cx->diag_mask, ind + 2); + cx->diag_mask = save_diag; + cx->exc_var = save_ev; + cx->exc_varlen = save_evl; + + appendStringInfoSpaces(&arms, (ind + 1) * 2); + appendStringInfo(&arms, "WHEN %s THEN\n", cond); + appendStringInfoString(&arms, pfx); + appendStringInfoString(&arms, hb); + } + nrescue++; + } + + if (cx->t[cx->pos].kind == T_KW && cx->t[cx->pos].kw == KW_ENSURE) + { + cx->pos++; + ensure_body = capture_block(cx, ind + 1); + } + + if (cx->t[cx->pos].kind == T_KW && cx->t[cx->pos].kw == KW_END) + cx->pos++; + else + plx_err(cx, bl, "unterminated begin block"); + + if (!ensure_body) + { + plx_indent(o, ind); + appendStringInfoString(o, "BEGIN\n"); + appendStringInfoString(o, body); + if (nrescue) + { + plx_indent(o, ind); + appendStringInfoString(o, "EXCEPTION\n"); + appendStringInfoString(o, arms.data); + } + plx_indent(o, ind); + appendStringInfoString(o, "END;\n"); + } + else + { + plx_indent(o, ind); + appendStringInfoString(o, "BEGIN\n"); + plx_indent(o, ind + 1); + appendStringInfoString(o, "BEGIN\n"); + appendStringInfoString(o, body); + if (nrescue) + { + plx_indent(o, ind + 1); + appendStringInfoString(o, "EXCEPTION\n"); + appendStringInfoString(o, arms.data); + } + plx_indent(o, ind + 1); + appendStringInfoString(o, "END;\n"); + appendStringInfoString(o, ensure_body); + plx_indent(o, ind); + appendStringInfoString(o, "EXCEPTION WHEN OTHERS THEN\n"); + appendStringInfoString(o, ensure_body); + plx_indent(o, ind + 1); + appendStringInfoString(o, "RAISE;\n"); + plx_indent(o, ind); + appendStringInfoString(o, "END;\n"); + } +} + +/* rewrite a comma-separated value list (for CASE WHEN v1, v2) */ +static char * +rewrite_value_list(Ctx *cx, int a, int b) +{ + StringInfoData out; + int i, seg = a, depth = 0, first = 1; + + initStringInfo(&out); + for (i = a; i <= b; i++) + { + if (i < b && (cx->t[i].kind == T_LPAREN || cx->t[i].kind == T_LBRACKET)) + depth++; + else if (i < b && (cx->t[i].kind == T_RPAREN || cx->t[i].kind == T_RBRACKET)) + depth--; + if (i == b || (depth == 0 && cx->t[i].kind == T_COMMA)) + { + char *st = plx_span_text(cx, seg, i); + + if (!first) + appendStringInfoString(&out, ", "); + appendStringInfoString(&out, plx_rewrite_expr(cx, st, (int) strlen(st), false)); + first = 0; + seg = i + 1; + } + } + return out.data; +} + +/* + * Ruby case/when -> plpgsql CASE. With a subject expression it is a simple CASE + * (WHEN value-list); without one it is a searched CASE (WHEN condition). + */ +static void +parse_case(Ctx *cx, int ind) +{ + StringInfo o = &cx->out; + int start = cx->pos; + int hdln = cx->t[start].line; + int e = plx_stmt_end(cx, start); + bool simple = (e > start + 1); + char *subj = NULL; + + if (simple) + { + char *st = plx_span_text(cx, start + 1, e); + + subj = plx_rewrite_expr(cx, st, (int) strlen(st), false); + } + cx->pos = e; + plx_skip_seps(cx); + + plx_indent(o, ind); + if (simple) + appendStringInfo(o, "CASE %s\n", subj); + else + appendStringInfoString(o, "CASE\n"); + + if (!(cx->t[cx->pos].kind == T_KW && cx->t[cx->pos].kw == KW_WHEN)) + plx_err(cx, hdln, "case requires at least one 'when'"); + + while (cx->t[cx->pos].kind == T_KW && cx->t[cx->pos].kw == KW_WHEN) + { + int ws = cx->pos; + int we = plx_stmt_end(cx, ws); + int va = ws + 1, vb = we; + char *vals; + + if (vb > va && cx->t[vb - 1].kind == T_KW && cx->t[vb - 1].kw == KW_THEN) + vb--; + if (va >= vb) + plx_err(cx, cx->t[ws].line, "when requires a value or condition"); + if (simple) + vals = rewrite_value_list(cx, va, vb); + else + { + char *st = plx_span_text(cx, va, vb); + + vals = plx_rewrite_expr(cx, st, (int) strlen(st), true); + } + plx_indent(o, ind + 1); + appendStringInfo(o, "WHEN %s THEN\n", vals); + cx->pos = we; + parse_block(cx, ind + 2); + } + if (cx->t[cx->pos].kind == T_KW && cx->t[cx->pos].kw == KW_ELSE) + { + plx_indent(o, ind + 1); + appendStringInfoString(o, "ELSE\n"); + cx->pos++; + parse_block(cx, ind + 2); + } + if (cx->t[cx->pos].kind == T_KW && cx->t[cx->pos].kw == KW_END) + cx->pos++; + else + plx_err(cx, hdln, "unterminated case block"); + plx_indent(o, ind); + appendStringInfoString(o, "END CASE;\n"); +} + +/* parse statements until a closer keyword or EOF (does not consume closer) */ +static void +parse_block(Ctx *cx, int ind) +{ + for (;;) + { + Tok *tk; + + plx_skip_seps(cx); + tk = &cx->t[cx->pos]; + if (tk->kind == T_EOF) + return; + if (tk->kind == T_KW && + (tk->kw == KW_END || tk->kw == KW_ELSIF || tk->kw == KW_ELSE || + tk->kw == KW_WHEN || tk->kw == KW_RESCUE || tk->kw == KW_ENSURE)) + return; + parse_stmt(cx, ind, false); + } +} + +static void +parse_stmt_inner(Ctx *cx, int ind, bool toplevel) +{ + Tok *tk; + + plx_skip_seps(cx); + tk = &cx->t[cx->pos]; + if (tk->kind == T_EOF) + return; + /* loop label: name: for i in ... / name: while ... / name: loop do */ + if (tk->kind == T_IDENT && plx_tok_is(&cx->t[cx->pos + 1], ":") && + cx->t[cx->pos + 2].kind == T_KW && + (cx->t[cx->pos + 2].kw == KW_FOR || cx->t[cx->pos + 2].kw == KW_WHILE || + cx->t[cx->pos + 2].kw == KW_UNTIL || cx->t[cx->pos + 2].kw == KW_LOOP)) + { + plx_indent(&cx->out, ind); + appendStringInfo(&cx->out, "<<%.*s>>\n", tk->len, tk->s); + cx->pos += 2; + tk = &cx->t[cx->pos]; + } + if (tk->kind == T_KW) + { + switch (tk->kw) + { + case KW_IF: + case KW_UNLESS: + /* could be a block opener OR a leaf w/ modifier — decide: + * modifier form has the opener keyword NOT at stmt start. Here + * tk IS at stmt start, so it's a block opener. */ + parse_if(cx, ind); + return; + case KW_WHILE: + case KW_UNTIL: + parse_while(cx, ind); + return; + case KW_FOR: + parse_for(cx, ind); + return; + case KW_LOOP: + parse_loop(cx, ind); + return; + case KW_DEF: + plx_err(cx, tk->line, "nested 'def' is not supported"); + return; + case KW_CASE: + parse_case(cx, ind); + return; + case KW_BEGIN: + parse_begin(cx, ind); + return; + default: + break; + } + } + /* leaf statement, or an iterator block (`... .each do |v| ... end`) */ + { + int a = cx->pos; + int b = plx_stmt_end(cx, a); + int i, do_pos = -1, depth = 0; + + for (i = a; i < b; i++) + { + if (cx->t[i].kind == T_LPAREN || cx->t[i].kind == T_LBRACKET) + depth++; + else if (cx->t[i].kind == T_RPAREN || cx->t[i].kind == T_RBRACKET) + depth--; + else if (depth == 0 && cx->t[i].kind == T_KW && cx->t[i].kw == KW_DO) + { + do_pos = i; + break; + } + } + if (do_pos >= 0) + parse_iter(cx, a, do_pos, b, ind); + else + { + plx_emit_leaf(cx, a, b, ind, toplevel); + cx->pos = b; + } + } +} + + +void +plx_ruby_parse_body(Ctx *cx) +{ + plx_lex(cx); + /* keyword-end (Ruby) top-level walk */ + for (;;) + { + plx_skip_seps(cx); + if (cx->t[cx->pos].kind == T_EOF) + break; + if (cx->t[cx->pos].kind == T_KW && + (cx->t[cx->pos].kw == KW_END || cx->t[cx->pos].kw == KW_ELSIF || + cx->t[cx->pos].kw == KW_ELSE || cx->t[cx->pos].kw == KW_WHEN || + cx->t[cx->pos].kw == KW_RESCUE || cx->t[cx->pos].kw == KW_ENSURE)) + plx_err(cx, cx->t[cx->pos].line, "unexpected '%.*s'", + cx->t[cx->pos].len, cx->t[cx->pos].s); + parse_stmt(cx, 1, true); + } +} + + static const PlxSurface ruby_surface = { .lanname = "plxruby", .block_style = PLX_BLK_KEYWORD_END, @@ -47,6 +804,7 @@ static const PlxSurface ruby_surface = { .excs = NULL, .nexcs = 0, .flags = PLX_TRUSTED, + .parse_body = plx_ruby_parse_body, }; static char * diff --git a/src/plx_dialect_ts.c b/src/plx_dialect_ts.c index 1143efd..184644b 100644 --- a/src/plx_dialect_ts.c +++ b/src/plx_dialect_ts.c @@ -3,10 +3,10 @@ * * TypeScript is the plxjs (JavaScript) surface plus type annotations: a * declaration "let x: T = e" carries the type on the variable. The TS - * preprocessor in plx_transpile.c (ts_types) rewrites those "id: T" annotations - * into the JS leading-colon-colon block-comment form and maps the TypeScript - * type to a SQL type, then the shared brace parser does the rest. Everything - * else is plxjs. + * preprocessor (ts_types, in plx_parse_brace.c) rewrites those "id: T" + * annotations into the JS leading-colon-colon block-comment form and maps the + * TypeScript type to a SQL type, then the shared brace parser does the rest. + * Everything else is plxjs. */ #include "postgres.h" @@ -15,6 +15,7 @@ #include "plx.h" #include "plx_int.h" +#include "plx_engine.h" PG_FUNCTION_INFO_V1(plx_ts_validator); PG_FUNCTION_INFO_V1(plx_ts_inline_handler); @@ -51,6 +52,7 @@ static const PlxSurface ts_surface = { .excs = NULL, .nexcs = 0, .flags = PLX_TRUSTED, + .parse_body = plx_brace_parse_body, }; static char * diff --git a/src/plx_dialect_tsql.c b/src/plx_dialect_tsql.c index ec29063..a41691a 100644 --- a/src/plx_dialect_tsql.c +++ b/src/plx_dialect_tsql.c @@ -4,21 +4,1533 @@ * T-SQL needs real restructuring to become plpgsql: @local variables and inline * DECLARE must be hoisted, IF/WHILE bodies use BEGIN..END rather than * THEN..END IF / LOOP..END LOOP, and TRY/CATCH becomes an EXCEPTION block. Like - * COBOL it therefore has its own tokenizer and recursive-descent front end in - * plx_transpile.c (block_style TSQL); this surface is a marker. The shared + * COBOL it therefore has its own tokenizer and recursive-descent front end + * (below); the dialect-neutral engine lives in plx_transpile.c. The shared * keyword table is unused by the T-SQL path but kept for consistency. */ #include "postgres.h" #include "fmgr.h" +#include "lib/stringinfo.h" +#include "miscadmin.h" #include "utils/memutils.h" #include "plx.h" #include "plx_int.h" +#include "plx_engine.h" PG_FUNCTION_INFO_V1(plx_tsql_validator); PG_FUNCTION_INFO_V1(plx_tsql_inline_handler); +/* ======================================================================== */ +/* T-SQL front end (plxtsql, Transact-SQL: SQL Server / Sybase). */ +/* */ +/* T-SQL differs from plpgsql in ways that need real restructuring, so like */ +/* COBOL it uses its own tokenizer and recursive-descent parser and emits */ +/* plpgsql directly: */ +/* - @local variables (and inline DECLARE anywhere) -> hoisted DECLARE */ +/* block; @x references drop the sigil. */ +/* - SET @x = e / SET @x += e -> x := e; / x := x + (e); */ +/* - SELECT @x = e [, ...] [FROM ...] -> x := e; or SELECT ... INTO */ +/* - IF cond stmt|BEGIN..END [ELSE ...] -> IF cond THEN ... END IF; */ +/* - WHILE cond stmt|BEGIN..END -> WHILE cond LOOP ... END LOOP; */ +/* - BEGIN TRY..END TRY BEGIN CATCH..END CATCH -> BEGIN..EXCEPTION..END */ +/* - PRINT e -> RAISE NOTICE; RAISERROR/THROW -> RAISE EXCEPTION; */ +/* - a library of type names (INT, NVARCHAR(n), DATETIME, ...) and */ +/* functions (ISNULL, LEN, GETDATE, IIF, CONVERT, ...). */ +/* Populates cx->locals + cx->out; the shared assemble path wraps it. */ +/* ======================================================================== */ + +typedef enum +{ + TQ_EOF, TQ_WORD, TQ_VAR, TQ_GVAR, TQ_NUM, TQ_STR, TQ_OP, + TQ_LP, TQ_RP, TQ_COMMA, TQ_SEMI, TQ_DOT +} TqKind; + +typedef struct +{ + TqKind kind; + const char *s; + int len; + int line; +} TqTok; + +typedef struct +{ + Ctx *cx; + TqTok *t; + int nt, pos; +} Tq; + +static void tq_stmt(Tq *tq, int ind); +static void tq_block(Tq *tq, int ind); +static void tq_body(Tq *tq, int ind); +static void tq_emit_range(Tq *tq, int a, int b, StringInfo out); + +static TqTok * +tq_cur(Tq *tq) +{ + return &tq->t[tq->pos]; +} + +/* case-insensitive: is token tk exactly the word w? */ +static bool +tq_ci(TqTok *tk, const char *w) +{ + int l = (int) strlen(w); + + return tk->kind == TQ_WORD && tk->len == l && pg_strncasecmp(tk->s, w, l) == 0; +} + +pg_noreturn static void +tq_err(Tq *tq, const char *msg) +{ + plx_err(tq->cx, tq->t[tq->pos].line, "%s", msg); +} + +/* lower-cased identifier from a token's raw text (for @vars and their decls) */ +static char * +tq_ident(const char *s, int len) +{ + char *r = palloc(len + 1); + int i; + + for (i = 0; i < len; i++) + { + char c = s[i]; + + r[i] = (c >= 'A' && c <= 'Z') ? (c - 'A' + 'a') : c; + } + r[len] = '\0'; + return r; +} + +static void +tq_lex(Tq *tq, const char *body) +{ + const char *p = body, + *end = body + strlen(body); + int line = 1, + cap = 256, + n = 0; + TqTok *t = palloc(sizeof(TqTok) * cap); + +#define TQPUSH(k, st, ln) do { \ + if (n >= cap) { cap *= 2; t = repalloc(t, sizeof(TqTok) * cap); } \ + t[n].kind = (k); t[n].s = (st); t[n].len = (ln); t[n].line = line; n++; \ + } while (0) +#define TQALPHA(c) (((c) >= 'A' && (c) <= 'Z') || ((c) >= 'a' && (c) <= 'z') || (c) == '_') +#define TQDIGIT(c) ((c) >= '0' && (c) <= '9') + + while (p < end) + { + if (*p == '\n') + { + line++; + p++; + continue; + } + if (*p == ' ' || *p == '\t' || *p == '\r') + { + p++; + continue; + } + if (p[0] == '-' && p + 1 < end && p[1] == '-') /* -- line comment */ + { + while (p < end && *p != '\n') + p++; + continue; + } + if (p[0] == '/' && p + 1 < end && p[1] == '*') /* block comment, nestable */ + { + int d = 1; + + p += 2; + while (p < end && d > 0) + { + if (p + 1 < end && p[0] == '/' && p[1] == '*') + { + d++; + p += 2; + } + else if (p + 1 < end && p[0] == '*' && p[1] == '/') + { + d--; + p += 2; + } + else + { + if (*p == '\n') + line++; + p++; + } + } + continue; + } + if (*p == '@') /* @@global or @local */ + { + if (p + 1 < end && p[1] == '@') + { + const char *st = p + 2, + *q = st; + + while (q < end && (TQALPHA(*q) || TQDIGIT(*q))) + q++; + TQPUSH(TQ_GVAR, st, (int) (q - st)); + p = q; + continue; + } + else + { + const char *st = p + 1, + *q = st; + + while (q < end && (TQALPHA(*q) || TQDIGIT(*q) || *q == '@' || *q == '#' || *q == '$')) + q++; + TQPUSH(TQ_VAR, st, (int) (q - st)); + p = q; + continue; + } + } + if (*p == '[') /* [delimited identifier] */ + { + const char *st = p + 1, + *q = st; + + while (q < end && *q != ']') + q++; + TQPUSH(TQ_WORD, st, (int) (q - st)); + p = (q < end) ? q + 1 : q; + continue; + } + if (*p == '"') /* "quoted identifier" */ + { + const char *st = p + 1, + *q = st; + + while (q < end && *q != '"') + q++; + TQPUSH(TQ_WORD, st, (int) (q - st)); + p = (q < end) ? q + 1 : q; + continue; + } + if (*p == 'N' && p + 1 < end && p[1] == '\'') /* N'unicode' -> drop N */ + p++; + if (*p == '\'') /* string literal */ + { + const char *st = p; + bool closed = false; + + p++; + while (p < end) + { + if (*p == '\'') + { + if (p + 1 < end && p[1] == '\'') /* doubled quote */ + { + p += 2; + continue; + } + p++; + closed = true; + break; + } + if (*p == '\n') + line++; + p++; + } + if (!closed) + plx_err(tq->cx, line, "unterminated string literal"); + TQPUSH(TQ_STR, st, (int) (p - st)); + continue; + } + if (TQALPHA(*p) || *p == '#') /* word / #temp */ + { + const char *st = p; + + while (p < end && (TQALPHA(*p) || TQDIGIT(*p) || *p == '#' || *p == '$')) + p++; + TQPUSH(TQ_WORD, st, (int) (p - st)); + continue; + } + if (TQDIGIT(*p) || (*p == '.' && p + 1 < end && TQDIGIT(p[1]))) + { + const char *st = p; + + while (p < end && (TQDIGIT(*p) || *p == '.' || + ((*p == 'e' || *p == 'E') && p + 1 < end) || + *p == 'x' || *p == 'X' || + (TQALPHA(*p) && st[0] == '0' && (st[1] == 'x' || st[1] == 'X')))) + p++; + TQPUSH(TQ_NUM, st, (int) (p - st)); + continue; + } + if (*p == '(') + { + TQPUSH(TQ_LP, p, 1); + p++; + continue; + } + if (*p == ')') + { + TQPUSH(TQ_RP, p, 1); + p++; + continue; + } + if (*p == ',') + { + TQPUSH(TQ_COMMA, p, 1); + p++; + continue; + } + if (*p == ';') + { + TQPUSH(TQ_SEMI, p, 1); + p++; + continue; + } + if (*p == '.') + { + TQPUSH(TQ_DOT, p, 1); + p++; + continue; + } + { + /* two-character operators */ + static const char *ops2[] = { + "<=", ">=", "<>", "!=", "!<", "!>", + "+=", "-=", "*=", "/=", "%=", "||", "::", NULL + }; + int k; + bool matched = false; + + for (k = 0; ops2[k]; k++) + if (p + 1 < end && p[0] == ops2[k][0] && p[1] == ops2[k][1]) + { + TQPUSH(TQ_OP, ops2[k], 2); + p += 2; + matched = true; + break; + } + if (matched) + continue; + } + TQPUSH(TQ_OP, p, 1); /* single-char operator */ + p++; + } + TQPUSH(TQ_EOF, end, 0); + tq->t = t; + tq->nt = n; + tq->pos = 0; +#undef TQPUSH +#undef TQALPHA +#undef TQDIGIT +} + +/* T-SQL statement-starting keywords (bound a condition/value scan) */ +static bool +tq_is_stmt_kw(TqTok *tk) +{ + static const char *k[] = { + "SELECT", "SET", "UPDATE", "INSERT", "DELETE", "MERGE", "PRINT", + "RETURN", "IF", "WHILE", "EXEC", "EXECUTE", "DECLARE", "THROW", + "RAISERROR", "BREAK", "CONTINUE", "WAITFOR", "GOTO", "TRUNCATE", + "CREATE", "DROP", "ALTER", "OPEN", "CLOSE", "FETCH", "COMMIT", + "ROLLBACK", NULL + }; + int i; + + if (tk->kind != TQ_WORD) + return false; + for (i = 0; k[i]; i++) + if (tq_ci(tk, k[i])) + return true; + return false; +} + +/* append with a leading space when needed */ +static void +tq_sp(StringInfo out) +{ + char last = out->len ? out->data[out->len - 1] : ' '; + + if (out->len && last != ' ' && last != '(' && last != '.') + appendStringInfoChar(out, ' '); +} + +/* emit a T-SQL string literal (already 'quoted', ''-escaped) as SQL text */ +static void +tq_emit_str(TqTok *tk, StringInfo out) +{ + /* T-SQL single-quoted strings use '' for an embedded quote, exactly like + * SQL, so the literal passes through verbatim. */ + appendBinaryStringInfo(out, tk->s, tk->len); +} + +/* map a T-SQL base type name to a PostgreSQL type; NULL if unknown */ +static const char * +tq_base_type(const char *s, int len) +{ + struct + { + const char *tsql; + const char *pg; + } m[] = { + {"int", "integer"}, {"integer", "integer"}, + {"bigint", "bigint"}, {"smallint", "smallint"}, {"tinyint", "smallint"}, + {"bit", "boolean"}, + {"decimal", "numeric"}, {"numeric", "numeric"}, {"dec", "numeric"}, + {"money", "numeric(19,4)"}, {"smallmoney", "numeric(10,4)"}, + {"float", "double precision"}, {"real", "real"}, + {"date", "date"}, {"datetime", "timestamp"}, {"datetime2", "timestamp"}, + {"smalldatetime", "timestamp"}, {"datetimeoffset", "timestamptz"}, + {"time", "time"}, + {"char", "char"}, {"nchar", "char"}, + {"varchar", "varchar"}, {"nvarchar", "varchar"}, + {"text", "text"}, {"ntext", "text"}, {"sysname", "varchar"}, + {"uniqueidentifier", "uuid"}, {"xml", "xml"}, + {"binary", "bytea"}, {"varbinary", "bytea"}, {"image", "bytea"}, + {NULL, NULL} + }; + int i; + + for (i = 0; m[i].tsql; i++) + if ((int) strlen(m[i].tsql) == len && pg_strncasecmp(m[i].tsql, s, len) == 0) + return m[i].pg; + return NULL; +} + +/* read a type reference from the stream into dest, consuming its tokens */ +static void +tq_read_type(Tq *tq, StringInfo dest) +{ + TqTok *w = tq_cur(tq); + const char *base; + bool is_char = false, + is_bin = false; + + if (w->kind != TQ_WORD) + tq_err(tq, "expected a type name"); + base = tq_base_type(w->s, w->len); + is_char = (base && (strcmp(base, "varchar") == 0 || strcmp(base, "char") == 0)); + is_bin = (base && strcmp(base, "bytea") == 0); + if (base) + appendStringInfoString(dest, base); + else + { + /* unknown -> pass the identifier through (user-defined or PG type) */ + char *id = tq_ident(w->s, w->len); + + appendStringInfoString(dest, id); + } + tq->pos++; + if (tq_cur(tq)->kind == TQ_LP) + { + TqTok *inner = &tq->t[tq->pos + 1]; + + if (inner->kind == TQ_WORD && tq_ci(inner, "MAX")) + { + /* VARCHAR(MAX)/NVARCHAR(MAX) -> text; VARBINARY(MAX) -> bytea */ + dest->len = 0; + dest->data[0] = '\0'; + appendStringInfoString(dest, is_bin ? "bytea" : "text"); + while (tq_cur(tq)->kind != TQ_RP && tq_cur(tq)->kind != TQ_EOF) + tq->pos++; + if (tq_cur(tq)->kind == TQ_RP) + tq->pos++; + } + else if (base && !is_char && strcmp(base, "numeric") != 0) + { + /* fixed-width type carries no length in PG; drop the () */ + while (tq_cur(tq)->kind != TQ_RP && tq_cur(tq)->kind != TQ_EOF) + tq->pos++; + if (tq_cur(tq)->kind == TQ_RP) + tq->pos++; + } + else + { + /* char/numeric width passes through: (n) or (p,s) */ + appendStringInfoChar(dest, '('); + tq->pos++; + while (tq_cur(tq)->kind != TQ_RP && tq_cur(tq)->kind != TQ_EOF) + { + TqTok *x = tq_cur(tq); + + if (x->kind == TQ_COMMA) + appendStringInfoChar(dest, ','); + else + appendBinaryStringInfo(dest, x->s, x->len); + tq->pos++; + } + appendStringInfoChar(dest, ')'); + if (tq_cur(tq)->kind == TQ_RP) + tq->pos++; + } + } +} + +/* map a type reference spanning tokens [a,b) into dest (for CONVERT) */ +static void +tq_type_from_range(Tq *tq, int a, int b, StringInfo dest) +{ + TqTok *w; + const char *base; + + if (a >= b || tq->t[a].kind != TQ_WORD) + { + appendStringInfoString(dest, "text"); + return; + } + w = &tq->t[a]; + base = tq_base_type(w->s, w->len); + appendStringInfoString(dest, base ? base : tq_ident(w->s, w->len)); + if (base && (pg_strncasecmp(w->s, "char", 4) == 0 || + pg_strncasecmp(w->s, "varc", 4) == 0 || + pg_strncasecmp(w->s, "nvar", 4) == 0 || + pg_strncasecmp(w->s, "ncha", 4) == 0 || + strcmp(base, "numeric") == 0) && + a + 1 < b && tq->t[a + 1].kind == TQ_LP) + { + int i = a + 1; + + while (i < b && tq->t[i].kind != TQ_RP) + { + if (tq->t[i].kind == TQ_COMMA) + appendStringInfoChar(dest, ','); + else if (tq->t[i].kind != TQ_LP) + appendBinaryStringInfo(dest, tq->t[i].s, tq->t[i].len); + else + appendStringInfoChar(dest, '('); + i++; + } + appendStringInfoChar(dest, ')'); + } +} + +/* index of the ')' matching the '(' at lp, within [.,limit), or -1 */ +static int +tq_match_lp(Tq *tq, int lp, int limit) +{ + int depth = 0, + i; + + for (i = lp; i < limit; i++) + { + if (tq->t[i].kind == TQ_LP) + depth++; + else if (tq->t[i].kind == TQ_RP && --depth == 0) + return i; + } + return -1; +} + +/* emit a function-call rename for a WORD at index i (followed by '('), or + * return false to fall through to the generic path. On success sets *ni. */ +static bool +tq_emit_call(Tq *tq, int i, int b, StringInfo out, int *ni) +{ + TqTok *tk = &tq->t[i]; + int lp = i + 1, + close; + + if (lp >= b || tq->t[lp].kind != TQ_LP) + return false; + close = tq_match_lp(tq, lp, b); + if (close < 0) + return false; + + /* CONVERT(type, expr [, style]) -> CAST(expr AS type) */ + if (tq_ci(tk, "CONVERT")) + { + int c1 = lp + 1, + depth = 0, + comma = -1, + comma2 = -1, + j; + StringInfoData ty; + + for (j = lp + 1; j < close; j++) + { + if (tq->t[j].kind == TQ_LP) + depth++; + else if (tq->t[j].kind == TQ_RP) + depth--; + else if (tq->t[j].kind == TQ_COMMA && depth == 0) + { + if (comma < 0) + comma = j; + else if (comma2 < 0) + comma2 = j; + } + } + if (comma < 0) + return false; + initStringInfo(&ty); + tq_type_from_range(tq, c1, comma, &ty); + tq_sp(out); + appendStringInfoString(out, "CAST("); + tq_emit_range(tq, comma + 1, comma2 > 0 ? comma2 : close, out); + appendStringInfo(out, " AS %s)", ty.data); + *ni = close + 1; + return true; + } + + /* IIF(cond, a, b) -> CASE WHEN cond THEN a ELSE b END */ + if (tq_ci(tk, "IIF")) + { + int depth = 0, + c1 = -1, + c2 = -1, + j; + + for (j = lp + 1; j < close; j++) + { + if (tq->t[j].kind == TQ_LP) + depth++; + else if (tq->t[j].kind == TQ_RP) + depth--; + else if (tq->t[j].kind == TQ_COMMA && depth == 0) + { + if (c1 < 0) + c1 = j; + else if (c2 < 0) + c2 = j; + } + } + if (c1 < 0 || c2 < 0) + return false; + tq_sp(out); + appendStringInfoString(out, "CASE WHEN"); + tq_emit_range(tq, lp + 1, c1, out); + appendStringInfoString(out, " THEN"); + tq_emit_range(tq, c1 + 1, c2, out); + appendStringInfoString(out, " ELSE"); + tq_emit_range(tq, c2 + 1, close, out); + appendStringInfoString(out, " END"); + *ni = close + 1; + return true; + } + + /* ERROR_MESSAGE() -> SQLERRM (no parens) */ + if (tq_ci(tk, "ERROR_MESSAGE") && close == lp + 1) + { + tq_sp(out); + appendStringInfoString(out, "SQLERRM"); + *ni = close + 1; + return true; + } + + /* simple 1:1 renames keeping the argument list */ + { + struct + { + const char *tsql; + const char *pg; + } r[] = { + {"isnull", "coalesce"}, {"len", "length"}, + {"datalength", "octet_length"}, {"getdate", "now"}, + {"sysdatetime", "now"}, {"getutcdate", "now"}, + {"newid", "gen_random_uuid"}, {"ceiling", "ceil"}, + {"charindex", NULL}, {NULL, NULL} + }; + int k; + + for (k = 0; r[k].tsql; k++) + if (tq_ci(tk, r[k].tsql)) + { + /* CHARINDEX(sub, str) -> strpos(str, sub): swap the two args */ + if (pg_strcasecmp(r[k].tsql, "charindex") == 0) + { + int depth = 0, + comma = -1, + j; + + for (j = lp + 1; j < close; j++) + { + if (tq->t[j].kind == TQ_LP) + depth++; + else if (tq->t[j].kind == TQ_RP) + depth--; + else if (tq->t[j].kind == TQ_COMMA && depth == 0 && comma < 0) + comma = j; + } + if (comma < 0) + return false; + tq_sp(out); + appendStringInfoString(out, "strpos("); + tq_emit_range(tq, comma + 1, close, out); + appendStringInfoString(out, ","); + tq_emit_range(tq, lp + 1, comma, out); + appendStringInfoChar(out, ')'); + *ni = close + 1; + return true; + } + tq_sp(out); + appendStringInfoString(out, r[k].pg); + appendStringInfoChar(out, '('); + tq_emit_range(tq, lp + 1, close, out); + appendStringInfoChar(out, ')'); + *ni = close + 1; + return true; + } + } + return false; +} + +/* emit tokens [a,b) as a plpgsql expression */ +static void +tq_emit_range(Tq *tq, int a, int b, StringInfo out) +{ + int i = a; + + while (i < b) + { + TqTok *tk = &tq->t[i]; + + if (tk->kind == TQ_VAR) + { + tq_sp(out); + appendStringInfoString(out, tq_ident(tk->s, tk->len)); + i++; + continue; + } + if (tk->kind == TQ_GVAR) + { + if (tk->len == 8 && pg_strncasecmp(tk->s, "IDENTITY", 8) == 0) + { + tq_sp(out); + appendStringInfoString(out, "lastval()"); + i++; + continue; + } + plx_err(tq->cx, tk->line, + "the T-SQL global variable @@%.*s is not supported", + tk->len, tk->s); + } + if (tk->kind == TQ_WORD) + { + int ni; + + if (tq_emit_call(tq, i, b, out, &ni)) + { + i = ni; + continue; + } + /* GETDATE with no arg list still means now() */ + if (tq_ci(tk, "GETDATE") || tq_ci(tk, "SYSDATETIME")) + { + tq_sp(out); + appendStringInfoString(out, "now()"); + i++; + continue; + } + tq_sp(out); + appendBinaryStringInfo(out, tk->s, tk->len); + i++; + continue; + } + if (tk->kind == TQ_NUM) + { + tq_sp(out); + appendBinaryStringInfo(out, tk->s, tk->len); + i++; + continue; + } + if (tk->kind == TQ_STR) + { + tq_sp(out); + tq_emit_str(tk, out); + i++; + continue; + } + if (tk->kind == TQ_LP) + { + tq_sp(out); + appendStringInfoChar(out, '('); + i++; + continue; + } + if (tk->kind == TQ_RP) + { + appendStringInfoChar(out, ')'); + i++; + continue; + } + if (tk->kind == TQ_COMMA) + { + appendStringInfoChar(out, ','); + i++; + continue; + } + if (tk->kind == TQ_DOT) + { + appendStringInfoChar(out, '.'); + i++; + continue; + } + if (tk->kind == TQ_OP) + { + tq_sp(out); + if (tk->len == 2 && tk->s[0] == '!' && tk->s[1] == '=') + appendStringInfoString(out, "<>"); + else + appendBinaryStringInfo(out, tk->s, tk->len); + i++; + continue; + } + i++; + } +} + +/* end of an expression/raw statement: first depth-0 ; EOF END or ELSE */ +static int +tq_value_end(Tq *tq, int from) +{ + int i = from, + depth = 0; + + while (i < tq->nt) + { + TqTok *tk = &tq->t[i]; + + if (tk->kind == TQ_LP) + depth++; + else if (tk->kind == TQ_RP) + { + if (depth > 0) + depth--; + } + else if (depth == 0) + { + if (tk->kind == TQ_SEMI || tk->kind == TQ_EOF || + tq_ci(tk, "END") || tq_ci(tk, "ELSE")) + return i; + } + i++; + } + return tq->nt - 1; +} + +/* end of an IF/WHILE condition: where the body (a keyword or BEGIN) begins */ +static int +tq_cond_end(Tq *tq, int from) +{ + int i = from, + depth = 0; + + while (i < tq->nt) + { + TqTok *tk = &tq->t[i]; + + if (tk->kind == TQ_LP) + depth++; + else if (tk->kind == TQ_RP) + { + if (depth > 0) + depth--; + } + else if (depth == 0) + { + if (tk->kind == TQ_SEMI || tk->kind == TQ_EOF || + tq_ci(tk, "END") || tq_ci(tk, "ELSE") || + tq_ci(tk, "BEGIN") || tq_is_stmt_kw(tk)) + return i; + } + i++; + } + return tq->nt - 1; +} + +static void +tq_eat_semi(Tq *tq) +{ + while (tq_cur(tq)->kind == TQ_SEMI) + tq->pos++; +} + +/* DECLARE @a T [= e] [, @b ...] */ +static void +tq_declare(Tq *tq, int ind) +{ + tq->pos++; /* DECLARE */ + for (;;) + { + TqTok *v = tq_cur(tq); + char *name; + PlxLocal2 *l; + StringInfoData ty; + + if (v->kind != TQ_VAR) + tq_err(tq, "expected an @variable after DECLARE"); + name = tq_ident(v->s, v->len); + tq->pos++; + if (tq_ci(tq_cur(tq), "AS")) /* DECLARE @c AS int (rare) */ + tq->pos++; + if (tq_ci(tq_cur(tq), "TABLE") || tq_ci(tq_cur(tq), "CURSOR")) + tq_err(tq, "DECLARE of a TABLE variable or CURSOR is not supported"); + initStringInfo(&ty); + tq_read_type(tq, &ty); + l = plx_local_add(tq->cx, name, (int) strlen(name)); + l->typ = ty.data; + if (tq_cur(tq)->kind == TQ_OP && tq_cur(tq)->len == 1 && tq_cur(tq)->s[0] == '=') + { + int e0 = tq->pos + 1, + e1; + + tq->pos++; + e1 = tq->pos; + /* value runs to a top-level comma or the statement terminator */ + { + int depth = 0; + + while (e1 < tq->nt) + { + TqTok *x = &tq->t[e1]; + + if (x->kind == TQ_LP) + depth++; + else if (x->kind == TQ_RP && depth > 0) + depth--; + else if (depth == 0 && + (x->kind == TQ_COMMA || x->kind == TQ_SEMI || + x->kind == TQ_EOF || tq_ci(x, "END"))) + break; + e1++; + } + } + plx_indent(&tq->cx->out, ind); + appendStringInfo(&tq->cx->out, "%s :=", name); + tq_emit_range(tq, e0, e1, &tq->cx->out); + appendStringInfoString(&tq->cx->out, ";\n"); + tq->pos = e1; + } + if (tq_cur(tq)->kind == TQ_COMMA) + { + tq->pos++; + continue; + } + break; + } + tq_eat_semi(tq); +} + +/* SET @x = e | SET @x += e | SET */ - if (g->t[h0].kind == GO_WORD && h0 + 2 <= semi1 && - g->t[h0 + 1].kind == GO_OP && - ((g->t[h0 + 1].len == 2 && g->t[h0 + 1].s[0] == ':' && g->t[h0 + 1].s[1] == '=') || - (g->t[h0 + 1].len == 1 && g->t[h0 + 1].s[0] == '='))) - ivar = h0; - /* post: ++ / -- / += S / -= S sets the direction */ - if (ivar >= 0 && g->t[semi2 + 1].kind == GO_WORD && - g->t[semi2 + 1].len == g->t[ivar].len && - strncmp(g->t[semi2 + 1].s, g->t[ivar].s, g->t[ivar].len) == 0 && - g->t[semi2 + 2].kind == GO_OP && g->t[semi2 + 2].len == 2) - { - GoTok *po = &g->t[semi2 + 2]; - - if (po->s[0] == '+' && po->s[1] == '+') - dir = 'u'; - else if (po->s[0] == '-' && po->s[1] == '-') - dir = 'd'; - else if (po->s[0] == '+' && po->s[1] == '=') - { - dir = 'u'; - step0 = semi2 + 3; - step1 = hb; - } - else if (po->s[0] == '-' && po->s[1] == '=') - { - dir = 'd'; - step0 = semi2 + 3; - step1 = hb; - } - } - if (!dir) - ivar = -1; - /* cond: />= (down), comparison immediately after var */ - if (ivar >= 0 && g->t[semi1 + 1].kind == GO_WORD && - g->t[semi1 + 1].len == g->t[ivar].len && - strncmp(g->t[semi1 + 1].s, g->t[ivar].s, g->t[ivar].len) == 0 && - g->t[semi1 + 2].kind == GO_OP && - (g->t[semi1 + 2].s[0] == '<' || g->t[semi1 + 2].s[0] == '>') && - (g->t[semi1 + 2].len == 1 || - (g->t[semi1 + 2].len == 2 && g->t[semi1 + 2].s[1] == '='))) - { - condc = g->t[semi1 + 2].s[0]; - inclusive = (g->t[semi1 + 2].len == 2); - } - else - ivar = -1; - /* the comparison must agree with the increment direction */ - if (ivar >= 0 && ((dir == 'u' && condc != '<') || (dir == 'd' && condc != '>'))) - ivar = -1; - - if (ivar >= 0) - { - indent(&g->cx->out, ind); - appendStringInfo(&g->cx->out, "FOR %s IN%s", go_ident(g->t[ivar].s, g->t[ivar].len), - dir == 'd' ? " REVERSE" : ""); - go_emit_range(g, ivar + 2, semi1, &g->cx->out); /* A (loop start) */ - appendStringInfoString(&g->cx->out, " .."); - if (inclusive) - go_emit_range(g, semi1 + 3, semi2, &g->cx->out); /* B (bound) */ - else - { - appendStringInfoString(&g->cx->out, " ("); - go_emit_range(g, semi1 + 3, semi2, &g->cx->out); /* B */ - appendStringInfo(&g->cx->out, ") %c 1", dir == 'd' ? '+' : '-'); - } - if (step0 >= 0) - { - if (step1 <= step0) - go_err(g, "for loop step (+=/-=) is missing its amount"); - appendStringInfoString(&g->cx->out, " BY"); - go_emit_range(g, step0, step1, &g->cx->out); - } - appendStringInfoString(&g->cx->out, " LOOP\n"); - g->pos = hb; - g->cx->loopdepth++; - go_block(g, ind + 1); - g->cx->loopdepth--; - indent(&g->cx->out, ind); - appendStringInfoString(&g->cx->out, "END LOOP;\n"); - go_eat_semi(g); - return; - } - } - - /* general C-style for -> [init;] WHILE cond LOOP body post; END LOOP. - * (continue re-tests the condition without running the post statement, as - * in plpgsql; use the counting form above when that matters.) */ - if (semi1 >= 0) - { - if (semi1 > h0) /* init statement */ - { - go_simple(g, ind, semi1); - } - g->pos = hb; - indent(&g->cx->out, ind); - appendStringInfoString(&g->cx->out, "WHILE"); - if (semi2 > semi1 + 1) - go_emit_range(g, semi1 + 1, semi2, &g->cx->out); - else - appendStringInfoString(&g->cx->out, " true"); - appendStringInfoString(&g->cx->out, " LOOP\n"); - go_block(g, ind + 1); - if (semi2 >= 0 && hb > semi2 + 1) /* post statement */ - { - int save = g->pos; - - g->pos = semi2 + 1; - go_simple(g, ind + 1, hb); - g->pos = save; - } - indent(&g->cx->out, ind); - appendStringInfoString(&g->cx->out, "END LOOP;\n"); - go_eat_semi(g); - return; - } - - /* for { } (infinite) or for cond { } (while) */ - indent(&g->cx->out, ind); - if (hb > h0) - { - appendStringInfoString(&g->cx->out, "WHILE"); - go_emit_range(g, h0, hb, &g->cx->out); - appendStringInfoString(&g->cx->out, " LOOP\n"); - } - else - appendStringInfoString(&g->cx->out, "LOOP\n"); - g->pos = hb; - g->cx->loopdepth++; - go_block(g, ind + 1); - g->cx->loopdepth--; - indent(&g->cx->out, ind); - appendStringInfoString(&g->cx->out, "END LOOP;\n"); - go_eat_semi(g); -} - -/* switch [tag] { case a, b: ...; default: ... } -> IF/ELSIF/ELSE */ -static void -go_switch(Go *g, int ind) -{ - int hb, - t0, - tag0 = -1, - tag1 = -1; - bool first = true, - had_default = false; - - g->pos++; /* switch */ - t0 = g->pos; - hb = go_header_end(g, g->pos); - if (hb > t0) /* a switch tag is present */ - { - tag0 = t0; - tag1 = hb; - } - if (go_cur(g)->kind != GO_LBRACE && g->t[hb].kind == GO_LBRACE) - g->pos = hb; - if (go_cur(g)->kind != GO_LBRACE) - go_err(g, "expected { after switch"); - g->pos++; /* { */ - go_eat_semi(g); - while (go_cur(g)->kind != GO_RBRACE && go_cur(g)->kind != GO_EOF) - { - bool is_default = go_ci(go_cur(g), "default"); - - if (!go_ci(go_cur(g), "case") && !is_default) - go_err(g, "expected case or default in switch"); - g->pos++; /* case / default */ - if (!is_default) - { - /* case values up to the ':' ; multiple values are OR-ed */ - int cv0 = g->pos, - colon = -1, - j, - depth = 0; - - for (j = g->pos; j < g->nt; j++) - { - GoKind k = g->t[j].kind; - - if (k == GO_LP || k == GO_LBRACK || k == GO_LBRACE) - depth++; - else if (k == GO_RP || k == GO_RBRACK || k == GO_RBRACE) - depth--; - else if (depth == 0 && k == GO_OP && g->t[j].len == 1 && g->t[j].s[0] == ':') - { - colon = j; - break; - } - } - if (colon < 0) - go_err(g, "expected : after case"); - indent(&g->cx->out, ind); - appendStringInfoString(&g->cx->out, first ? "IF" : "ELSIF"); - /* split the case values on top-level commas */ - { - int s = cv0, - d = 0, - nc = 0; - - for (j = cv0; j <= colon; j++) - { - GoKind k = (j < colon) ? g->t[j].kind : GO_COMMA; - - if (k == GO_LP || k == GO_LBRACK) - d++; - else if (k == GO_RP || k == GO_RBRACK) - d--; - else if (k == GO_COMMA && d == 0) - { - if (nc) - appendStringInfoString(&g->cx->out, " OR"); - if (tag0 >= 0) - { - appendStringInfoChar(&g->cx->out, ' '); - go_emit_range(g, tag0, tag1, &g->cx->out); - appendStringInfoString(&g->cx->out, " ="); - } - go_emit_range(g, s, j, &g->cx->out); - s = j + 1; - nc++; - } - } - } - appendStringInfoString(&g->cx->out, " THEN\n"); - g->pos = colon + 1; - first = false; - } - else - { - int colon = -1, - j; - - for (j = g->pos; j < g->nt; j++) - if (g->t[j].kind == GO_OP && g->t[j].len == 1 && g->t[j].s[0] == ':') - { - colon = j; - break; - } - if (colon < 0) - go_err(g, "expected : after default"); - indent(&g->cx->out, ind); - appendStringInfoString(&g->cx->out, first ? "IF true THEN\n" : "ELSE\n"); - g->pos = colon + 1; - had_default = true; - first = false; - } - go_eat_semi(g); - /* the case body runs until the next case/default or the closing } */ - while (!go_ci(go_cur(g), "case") && !go_ci(go_cur(g), "default") && - go_cur(g)->kind != GO_RBRACE && go_cur(g)->kind != GO_EOF) - { - if (go_ci(go_cur(g), "fallthrough")) - go_err(g, "fallthrough is not supported"); - go_stmt(g, ind + 1); - } - } - if (go_cur(g)->kind == GO_RBRACE) - g->pos++; - go_eat_semi(g); - if (!first) - { - (void) had_default; - indent(&g->cx->out, ind); - appendStringInfoString(&g->cx->out, "END IF;\n"); - } -} - -/* return [expr] */ -static void -go_return(Go *g, int ind) -{ - int e0, - e1; - - g->pos++; /* return */ - e0 = g->pos; - e1 = go_stmt_end(g, e0); - indent(&g->cx->out, ind); - if (e1 > e0) - { - if (g->cx->retset) - appendStringInfoString(&g->cx->out, "RETURN QUERY SELECT"); - else - appendStringInfoString(&g->cx->out, "RETURN"); - go_emit_range(g, e0, e1, &g->cx->out); - appendStringInfoString(&g->cx->out, ";\n"); - } - else - appendStringInfoString(&g->cx->out, "RETURN;\n"); - g->pos = e1; -} - -static void -go_stmt(Go *g, int ind) -{ - GoTok *tk = go_cur(g); - - if (++g->cx->depth > PLX_MAX_DEPTH) - go_err(g, "statement nested too deeply"); - - if (tk->kind == GO_SEMI) - { - g->pos++; - g->cx->depth--; - return; - } - if (tk->kind == GO_LBRACE) /* a bare block: inline its statements */ - { - go_block(g, ind); - go_eat_semi(g); - } - else if (go_ci(tk, "var")) - go_var(g, ind, false); - else if (go_ci(tk, "const")) - go_var(g, ind, true); - else if (go_ci(tk, "if")) - go_if(g, ind); - else if (go_ci(tk, "for")) - go_for(g, ind); - else if (go_ci(tk, "switch")) - go_switch(g, ind); - else if (go_ci(tk, "return")) - go_return(g, ind); - else if (go_ci(tk, "break")) - { - g->pos++; - indent(&g->cx->out, ind); - appendStringInfoString(&g->cx->out, "EXIT;\n"); - go_eat_semi(g); - } - else if (go_ci(tk, "continue")) - { - g->pos++; - indent(&g->cx->out, ind); - appendStringInfoString(&g->cx->out, "CONTINUE;\n"); - go_eat_semi(g); - } - else if (go_ci(tk, "func")) - go_err(g, "nested function definitions are not supported"); - else if (go_ci(tk, "go") || go_ci(tk, "defer") || go_ci(tk, "select") || - go_ci(tk, "goto")) - go_err(g, "go, defer, select, and goto are not supported"); - else - { - go_simple(g, ind, -1); - go_eat_semi(g); - } - - g->cx->depth--; -} - -/* a brace-delimited block: { statements } */ -static void -go_block(Go *g, int ind) -{ - if (go_cur(g)->kind != GO_LBRACE) - go_err(g, "expected {"); - g->pos++; - go_eat_semi(g); - while (go_cur(g)->kind != GO_RBRACE && go_cur(g)->kind != GO_EOF) - go_stmt(g, ind); - if (go_cur(g)->kind != GO_RBRACE) - go_err(g, "missing }"); - g->pos++; -} - -static void -go_transpile(Ctx *cx) -{ - Go g; - - memset(&g, 0, sizeof(g)); - g.cx = cx; - go_lex(&g, cx->body); - go_eat_semi(&g); - - /* an outer { ... } wrapping the whole body is unwrapped */ - if (go_cur(&g)->kind == GO_LBRACE) - { - int close = go_match(&g, g.pos, g.nt); - - if (close == g.nt - 2 || (close > 0 && g.t[close + 1].kind == GO_SEMI && - close + 2 >= g.nt - 1)) - { - g.pos++; - while (go_cur(&g)->kind != GO_RBRACE && go_cur(&g)->kind != GO_EOF) - go_stmt(&g, 1); - return; - } - } - while (go_cur(&g)->kind != GO_EOF) - { - if (go_cur(&g)->kind == GO_RBRACE) - go_err(&g, "unexpected }"); - go_stmt(&g, 1); - } -} - -/* ======================================================================== */ - -bool -plx_has_sentinel(const char *prosrc) -{ - while (*prosrc == ' ' || *prosrc == '\n' || *prosrc == '\t' || *prosrc == '\r') - prosrc++; - return strncmp(prosrc, "/*plx:v1:", 9) == 0; -} - -static unsigned int -djb2(const char *s) -{ - unsigned int h = 5381; - - while (*s) - h = ((h << 5) + h) + (unsigned char) *s++; - return h; -} - -char * -plx_transpile(const char *body, const PlxFuncMeta *meta, const PlxSurface *surf, - MemoryContext scratch) -{ - Ctx cx; - StringInfoData asm_; - PlxLocal2 *l; - - memset(&cx, 0, sizeof(cx)); - cx.body = body; - cx.meta = meta; - cx.surf = surf; - cx.retset = meta ? meta->retset : false; - cx.mcx = scratch; - initStringInfo(&cx.out); - - if (surf->block_style == PLX_BLK_COBOL) - { - cobol_transpile(&cx); - goto assemble; - } - if (surf->block_style == PLX_BLK_TSQL) - { - tsql_transpile(&cx); - goto assemble; - } - if (surf->block_style == PLX_BLK_GO) - { - go_transpile(&cx); - goto assemble; - } - if (surf->block_style == PLX_BLK_PLSQL) - { - /* PL/SQL already carries its own DECLARE/BEGIN/END; emit it directly */ - plsql_transpile(&cx); - initStringInfo(&asm_); - appendStringInfo(&asm_, "/*plx:v1:%s:%08x*/\n", surf->lanname, djb2(body)); + appendStringInfoString(&asm_, "BEGIN\n"); appendStringInfoString(&asm_, cx.out.data); - if (asm_.len == 0 || asm_.data[asm_.len - 1] != '\n') - appendStringInfoChar(&asm_, '\n'); - appendStringInfo(&asm_, "/*plx-orig:b64$%s$plx-orig*/\n", b64_encode_body(body)); - return asm_.data; - } - - /* TypeScript: rewrite "id: T" annotations before lexing (the original TS is - * still embedded via the unchanged `body`). */ - if (surf->ts_types) - cx.body = ts_preprocess(body); - - lex(&cx); - - if (surf->block_style == PLX_BLK_BRACE) - parse_brace_program(&cx); - else if (surf->block_style == PLX_BLK_INDENT) - parse_py_program(&cx); - else - { - /* keyword-end (Ruby) top-level walk */ - for (;;) - { - skip_seps(&cx); - if (cx.t[cx.pos].kind == T_EOF) - break; - if (cx.t[cx.pos].kind == T_KW && - (cx.t[cx.pos].kw == KW_END || cx.t[cx.pos].kw == KW_ELSIF || - cx.t[cx.pos].kw == KW_ELSE || cx.t[cx.pos].kw == KW_WHEN || - cx.t[cx.pos].kw == KW_RESCUE || cx.t[cx.pos].kw == KW_ENSURE)) - plx_err(&cx, cx.t[cx.pos].line, "unexpected '%.*s'", - cx.t[cx.pos].len, cx.t[cx.pos].s); - parse_stmt(&cx, 1, true); - } - } - -assemble: - /* assemble: sentinel + DECLARE + BEGIN + body + END + embedded original */ - initStringInfo(&asm_); - appendStringInfo(&asm_, "/*plx:v1:%s:%08x*/\n", surf->lanname, djb2(body)); - if (cx.locals) - { - appendStringInfoString(&asm_, "DECLARE\n"); - for (l = cx.locals; l; l = l->next) - { - if (l->is_record) - appendStringInfo(&asm_, " %s RECORD;\n", l->name); - else if (!l->typ) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("%s: cannot infer a PostgreSQL type for local variable \"%s\"", - surf->lanname, l->name), - errhint("add an explicit type annotation for \"%s\"", l->name))); - else if (l->is_const) - appendStringInfo(&asm_, " %s CONSTANT %s := %s;\n", l->name, l->typ, l->init); - else if (l->init) - appendStringInfo(&asm_, " %s %s := %s;\n", l->name, l->typ, l->init); - else - appendStringInfo(&asm_, " %s %s;\n", l->name, l->typ); - } + appendStringInfoString(&asm_, "END;\n"); } - appendStringInfoString(&asm_, "BEGIN\n"); - appendStringInfoString(&asm_, cx.out.data); - appendStringInfoString(&asm_, "END;\n"); appendStringInfo(&asm_, "/*plx-orig:b64$%s$plx-orig*/\n", b64_encode_body(body)); return asm_.data;