Files
JetCore/jetc/JetCodegen.cpp
Brad Arant b0cadc3f08 Multi-day accumulated fixes: tag dispatch, session/pooling correctness, SPA rollout, jetc compiler
JetCore engine (Tag.cpp/.h, Global.cpp/.h, __*.cpp tag classes):
- Numerous __mysql.cpp connection-pooling correctness fixes (stale
  result sets, double-indirection sessionid bugs, field caching,
  pool keying) and a session-registration-before-connection-confirmed
  ordering fix (dangling pointer on a failed connect)
- Tag.cpp: large body of fixes to the tag-dispatch/container-scanning
  logic (bare-name fallback, database aliasing, single-quote handling,
  @-prefix session variable resolution, TAGDEF caching, nested split-tag
  scanning for if/while/until/ifrow so an inner one doesn't corrupt an
  outer one's own boundary scan) plus the process-first rendering
  order fix (a <process> now runs before <checksession>/page
  resolution, fixing a real "blank page after successful login" bug)
- Global.h/.cpp: added imapSessions (parallel to the existing __mysql*
  session map) for the new <imap> tag family
- __whiledir.cpp/__include.cpp: caching and directory-name-aliasing
  fixes
- __call.cpp/.h: fixed a dangling-pointer bug
- __write.cpp: fixed an O_WRONLY-only open that couldn't create a new
  file
- __ifrow.cpp/__sql.cpp: fixed the same sessionid double-indirection
  bug __mysql.cpp had (resolveKeyword's result was being used as a map
  key lookup instead of the resolved value itself)

jetc compiler (jetc/JetParser.cpp/.h, JetCodegen.cpp/.h, JetAST.h,
jetc.cpp, build-shared.sh, build-compiled.sh):
- Parser fixes: inline <include>, nested-quote handling, void-tag
  recognition
- Codegen fixes and expanded AST support
- build-shared.sh: atomic rename into place for a freshly-built .jtc
  (a live server dlopen()ing the same path mid-rebuild was corrupting
  its own already-mapped view of the old file) and a staleness check
  so a cached JetCore .o doesn't silently keep serving yesterday's
  fixes forever
- Added -lssl to both build scripts' link lines (needed once the new
  <imap> tag's client-side TLS code existed - previously nothing in
  this codebase used OpenSSL's SSL/TLS client API, only libcrypto)

Site content (jetc/barant-site/): SPA (fetch-and-swap navigation)
rollout across essentially every .page/.jet/.section/.tag/.proc/.method
file - dynamic-content fixes, fragment-mode stripping, native
$[@name] session variables replacing older mechanisms, and this
session's own checkout.page redesign, login start-param redirect fix,
and per-page side-menu (menu.bar/$[PAGE].menu) refresh support.
Completed the earlier main.css/control.css move from assets/ to
assets/images/ (git still showed the old paths as tracked-but-deleted
until now).

Deliberately left out of this commit: dozens of .pre-<description>-
<date> manual backup files (this session's and earlier ones' own
before/after snapshots, not meant for permanent history), compiled
.jtc build outputs, ad hoc test binaries and scripts under jetc/
(login_asan, wcwc_test, optest2_bin, etc.), and jetc/__pycache__/ -
none of these are meant to be tracked; a .gitignore covering them
would be worth adding separately.
2026-09-03 05:20:48 +00:00

308 lines
15 KiB
C++

#include "JetCodegen.h"
#include <stdexcept>
namespace jetc {
std::string Codegen::ind(int indent) {
return std::string(indent * 3, ' ');
}
std::string Codegen::cstr(const std::string &raw) {
std::string r;
r.reserve(raw.size() + 8);
r += '"';
for (unsigned char c : raw) {
switch (c) {
case '\\': r += "\\\\"; break;
case '"': r += "\\\""; break;
case '\n': r += "\\n"; break;
case '\r': r += "\\r"; break;
case '\t': r += "\\t"; break;
default:
if (c < 0x20) {
char buf[8];
snprintf(buf, sizeof(buf), "\\x%02x\" \"", c);
r += buf;
} else {
r += (char)c;
}
}
}
r += '"';
return r;
}
const char *Codegen::staticTypeOp(const std::string &type, const char *rc) {
// type is known at compile time (a literal attribute value, not a
// runtime expression) so the comparison operator is picked once here
// instead of being re-checked against six string constants on every
// request the way Tag::__if/__while do it at runtime.
static thread_local std::string expr;
if (type == "eq") expr = std::string(rc) + " == 0";
else if (type == "ne") expr = std::string(rc) + " != 0";
else if (type == "lt") expr = std::string(rc) + " == -1";
else if (type == "le") expr = std::string(rc) + " != 1";
else if (type == "gt") expr = std::string(rc) + " == 1";
else if (type == "ge") expr = std::string(rc) + " != -1";
else throw std::runtime_error("jetc: type=\"" + type + "\" is not one of eq/ne/lt/le/gt/ge "
"(and jetc only supports a literal type= value - use expr= for a dynamic comparison).");
return expr.c_str();
}
void Codegen::emitBody(const std::vector<NodePtr> &nodes, int indent, std::ostringstream &target) {
for (auto &n : nodes) emitNode(n, indent, target);
}
void Codegen::emitNode(const NodePtr &n, int indent, std::ostringstream &target) {
std::string I = ind(indent);
switch (n->kind) {
case NodeKind::TEXT: {
if (n->text.empty()) break;
target << I << "out << " << cstr(n->text) << ";\n";
break;
}
case NodeKind::VAR: {
target << I << "{ coreutils::ZString v(" << cstr(n->varText) << "); out << scope.getVariable(v, true); }\n";
break;
}
case NodeKind::SET: {
std::string k = id("set_");
target << I << "{\n";
target << I << " scope.keywords[\"" << k << "name\"] = " << cstr(n->setName) << ";\n";
target << I << " scope.keywords[\"" << k << "scope\"] = " << cstr(n->setScope) << ";\n";
target << I << " coreutils::MString val;\n";
if (n->setHasExpr) {
target << I << " scope.keywords[\"" << k << "expr\"] = " << cstr(n->setExpr) << ";\n";
target << I << " val = jet::Operand(scope.keywords[\"" << k << "expr\"], scope).string;\n";
} else {
target << I << " scope.keywords[\"" << k << "value\"] = " << cstr(n->setHasValue ? n->setValue : std::string()) << ";\n";
target << I << " val = scope.resolveKeyword(\"" << k << "value\");\n";
}
target << I << " scope.assignVariable(scope.resolveKeyword(\"" << k << "name\"), val, scope.resolveKeyword(\"" << k << "scope\"));\n";
target << I << "}\n";
break;
}
case NodeKind::IF: {
std::string k = id("if_");
target << I << "{\n";
target << I << " bool cond;\n";
if (n->cond.mode == CondMode::EXPR) {
target << I << " scope.keywords[\"" << k << "expr\"] = " << cstr(n->cond.expr) << ";\n";
target << I << " cond = jet::Operand(scope.keywords[\"" << k << "expr\"], scope).boolean;\n";
} else {
target << I << " scope.keywords[\"" << k << "value1\"] = " << cstr(n->cond.value1) << ";\n";
target << I << " scope.keywords[\"" << k << "value2\"] = " << cstr(n->cond.value2) << ";\n";
target << I << " int rc = scope.resolveKeyword(\"" << k << "value1\").compare(scope.resolveKeyword(\"" << k << "value2\"));\n";
target << I << " cond = (" << staticTypeOp(n->cond.type, "rc") << ");\n";
}
target << I << " if (cond) {\n";
emitBody(n->thenBody, indent + 2, target);
if (n->hasElse) {
target << I << " } else {\n";
emitBody(n->elseBody, indent + 2, target);
}
target << I << " }\n";
target << I << "}\n";
break;
}
case NodeKind::FOR: {
std::string k = id("for_");
target << I << "{\n";
target << I << " scope.keywords[\"" << k << "start\"] = " << cstr(n->forHasStart ? n->forStart : std::string("0")) << ";\n";
target << I << " scope.keywords[\"" << k << "end\"] = " << cstr(n->forEnd) << ";\n";
target << I << " scope.keywords[\"" << k << "step\"] = " << cstr(n->forStep) << ";\n";
target << I << " double ix = scope.resolveKeyword(\"" << k << "start\").asDouble();\n";
if (n->forHasName) {
target << I << " scope.keywords[\"" << k << "name\"] = " << cstr(n->forName) << ";\n";
target << I << " scope.keywords[\"" << k << "scope\"] = " << cstr(n->forScope) << ";\n";
}
target << I << " for (; ix <= scope.resolveKeyword(\"" << k << "end\").asDouble(); ix += scope.resolveKeyword(\"" << k << "step\").asDouble()) {\n";
if (n->forHasName) {
target << I << " coreutils::MString nameSel = scope.resolveKeyword(\"" << k << "name\");\n";
target << I << " coreutils::MString scopeSel = scope.resolveKeyword(\"" << k << "scope\");\n";
target << I << " if ((scopeSel == \"\") || (scopeSel == \"global\")) global.variables[nameSel] = ix;\n";
target << I << " else if (scopeSel == \"local\") scope.local->variables[nameSel] = ix;\n";
target << I << " else if (scopeSel == \"parent\") scope.parent->local->variables[nameSel] = ix;\n";
target << I << " else throw coreutils::Exception(\"scope value is not valid.\");\n";
}
emitBody(n->body, indent + 2, target);
target << I << " }\n";
target << I << "}\n";
break;
}
case NodeKind::WHILE: {
std::string k = id("while_");
target << I << "{\n";
target << I << " bool cond;\n";
if (n->cond.mode == CondMode::EXPR) {
target << I << " scope.keywords[\"" << k << "expr\"] = " << cstr(n->cond.expr) << ";\n";
target << I << " coreutils::MString exprSaved = scope.keywords[\"" << k << "expr\"];\n";
target << I << " cond = jet::Operand(scope.keywords[\"" << k << "expr\"], scope).boolean;\n";
target << I << " while (cond) {\n";
emitBody(n->body, indent + 2, target);
target << I << " scope.keywords[\"" << k << "expr\"].reset();\n";
target << I << " scope.keywords[\"" << k << "expr\"] = exprSaved;\n";
target << I << " cond = jet::Operand(scope.keywords[\"" << k << "expr\"], scope).boolean;\n";
target << I << " }\n";
} else {
target << I << " scope.keywords[\"" << k << "value1\"] = " << cstr(n->cond.value1) << ";\n";
target << I << " scope.keywords[\"" << k << "value2\"] = " << cstr(n->cond.value2) << ";\n";
target << I << " int rc = scope.resolveKeyword(\"" << k << "value1\").compare(scope.resolveKeyword(\"" << k << "value2\"));\n";
target << I << " cond = (" << staticTypeOp(n->cond.type, "rc") << ");\n";
target << I << " while (cond) {\n";
emitBody(n->body, indent + 2, target);
target << I << " int rc2 = scope.resolveKeyword(\"" << k << "value1\").compare(scope.resolveKeyword(\"" << k << "value2\"));\n";
target << I << " cond = (" << staticTypeOp(n->cond.type, "rc2") << ");\n";
target << I << " }\n";
}
target << I << "}\n";
break;
}
case NodeKind::FALLBACK: {
target << I << "{ coreutils::ZString span(" << cstr(n->rawSpan) << "); scope.interpretSpan(span, out); }\n";
break;
}
case NodeKind::CONTAINER_SLOT: {
// Reuses the interpreter's own <container> scope-walking logic
// verbatim (see Tag::renderContainer) rather than re-deriving
// it - this is exactly what the runtime's own <container>
// dispatch does when it's reached via interpretSpan, just
// without the text-scanning step to get there.
target << I << "scope.renderContainer(out);\n";
break;
}
case NodeKind::TAGDEF: {
// Codegen can still fail for a node that parsed successfully -
// e.g. an <if>/<while> whose type= isn't one of eq/ne/lt/le/gt/ge
// (staticTypeOp's own check - real legacy content occasionally
// has one of these sitting in a branch that's never actually
// reached at runtime either, so there's nothing to "correctly"
// compile). Parsing this definition's body already succeeded
// (see parseTagDef's own try/catch for that stage) or this node
// wouldn't exist as TAGDEF at all - so mirror that same
// graceful-degradation approach here for the emit stage: try
// building the compiled function into a throwaway stream first,
// and only splice it into tagDefFuncs (and register the
// compiledTags fast path) if that fully succeeds. A failure
// here just means this one definition stays interpreted,
// unchanged - zero risk to the rest of the file.
std::string funcName = "jetc_tagdef_" + id("");
std::ostringstream trial;
bool compiledOk = true;
try {
trial << "static void " << funcName << "(jet::Tag &scope, coreutils::MString &out) {\n";
emitBody(n->tagDefBody, 1, trial);
trial << "}\n\n";
} catch (const std::exception &) {
compiledOk = false;
}
// Emitted in place, at this TAGDEF's original position in
// program flow - matches exactly what FALLBACK would have
// emitted for this same <tag name="X">...</tag> span, so
// global.tags["x"] still gets populated as before (needed as a
// correctness fallback: any invocation this compiler doesn't
// itself recognize - e.g. reached through content this pass
// never compiled - still resolves it exactly as it always has).
target << I << "{ coreutils::ZString span(" << cstr(n->rawSpan) << "); scope.interpretSpan(span, out); }\n";
if (compiledOk) {
// The registration into compiledTags right after is the only
// new behavior beyond the raw registration above - a pure
// additive fast path __dotag checks first (see __dotag.cpp);
// a miss there falls through to the now-unchanged interpreted
// path, which is exactly what happens when compiledOk is
// false here too.
tagDefFuncs << trial.str();
target << I << "global.compiledTags[coreutils::MString(" << cstr(n->tagDefName) << ")] = &" << funcName << ";\n";
}
break;
}
}
}
std::string Codegen::generate(const std::vector<NodePtr> &top, const std::string &sourceFileComment,
bool sharedMode, const std::string &entrySymbol,
const std::vector<ApiRoute> &apiRoutes, const std::string &apiSymbol) {
out.str("");
out.clear();
tagDefFuncs.str("");
tagDefFuncs.clear();
out << "// " << sourceFileComment << "\n";
out << "// Generated by jetc - DO NOT EDIT BY HAND.\n";
out << "#include \"Global.h\"\n";
out << "#include \"Tag.h\"\n";
out << "#include \"Operand.h\"\n";
out << "#include \"ZString.h\"\n";
out << "#include \"MString.h\"\n";
out << "#include \"Exception.h\"\n";
out << "#include <iostream>\n\n";
// The entry function's own text is built into a separate stream so
// tagDefFuncs - populated as a SIDE EFFECT of the emitBody() call
// below, since TAGDEF nodes append to it as they're encountered -
// can be spliced in ahead of it. A compiled tag-definition function
// never needs to be forward-declared for anything defined before it
// in program order (nothing upstream calls it directly - see
// __dotag.cpp's runtime lookup instead), so this ordering is purely
// for readability, not a correctness requirement.
std::ostringstream body;
if (sharedMode) {
if (!apiRoutes.empty()) {
// A plain C struct, not any CoreUtils/JetCore type - this is the
// one piece of the plugin ABI a host loader needs to know about
// independent of the rest of this same-ABI C++ boundary.
body << "struct JetApiRoute { const char *method; const char *path; };\n";
body << "extern \"C\" const JetApiRoute *" << apiSymbol << "(int *count) {\n";
body << " static const JetApiRoute routes[] = {\n";
for (auto &r : apiRoutes)
body << " { " << cstr(r.method) << ", " << cstr(r.path) << " },\n";
body << " };\n";
body << " *count = " << apiRoutes.size() << ";\n";
body << " return routes;\n";
body << "}\n\n";
}
body << "extern \"C\" void " << entrySymbol << "(jet::Global &global, coreutils::MString &out) {\n";
body << " coreutils::ZString jetc_empty(\"\");\n";
body << " coreutils::MString jetc_scopeOut;\n";
body << " jet::Tag scope(jetc_empty, jetc_scopeOut, global, NULL, NULL);\n";
emitBody(top, 1, body);
body << "}\n";
return out.str() + tagDefFuncs.str() + body.str();
}
body << "static void jetc_render(jet::Global &global, coreutils::MString &out) {\n";
body << " coreutils::ZString jetc_empty(\"\");\n";
body << " coreutils::MString jetc_scopeOut;\n";
body << " jet::Tag scope(jetc_empty, jetc_scopeOut, global, NULL, NULL);\n";
emitBody(top, 1, body);
body << "}\n\n";
body << "int main(int argc, char **argv, char **envp) {\n";
body << " jet::Global global(envp);\n";
body << " coreutils::MString out;\n";
body << " try {\n";
body << " jetc_render(global, out);\n";
body << " global.outputHeaders();\n";
body << " std::cout << out;\n";
body << " } catch(coreutils::Exception e) {\n";
body << " std::cout << out;\n";
body << " std::cout << \"\\n----- Error: \" << e.text << std::endl;\n";
body << " return 1;\n";
body << " }\n";
body << " return 0;\n";
body << "}\n";
return out.str() + tagDefFuncs.str() + body.str();
}
}