Files
JetCore/jetc/JetCodegen.cpp
T
Brad ArantandClaude Sonnet 5 634eb7a9a9 jetc: add #api directive + jetc_api_<name> route metadata export
Stage 1 of the APIServer plan: a compiled --shared .so currently exports
only a bare jetc_render_<name>(Global&, MString&) with no way for a host
to discover what HTTP method+path it wants to serve. Adds a leading
"#api METHOD /path" directive (one or more, parsed the same way jetc
already strips a leading "#!" shebang line - not modeled as a new
AST/tag-attribute concept, since jetc's parser has no existing notion of
the enclosing <jet> tag's attributes to hang this off of).

In --shared mode, JetCodegen now also emits a second exported function
alongside the render entry point:

  struct JetApiRoute { const char *method; const char *path; };
  extern "C" const JetApiRoute *jetc_api_<name>(int *count) { ... }

<name> is derived the same way for both symbols (sanitizeIdent(
baseNameNoExt(inputPath))), so a future host loader can find both by
convention from just the .so's filename - no name needs to be embedded
in the metadata itself, and the routes a module declares can never drift
out of sync with the render function that implements them (the failure
mode a sidecar manifest file would have).

Malformed directives (missing method/path, path without a leading '/')
are a hard compile error; #api directives present without --shared print
a non-fatal warning (the metadata has nowhere to go without a host to
load a .so, but the render-only executable/CGI path still works fine).

Verified live, not just compiled: rebuilt jetc, compiled demo3.jet (a new
worked example with two #api lines sharing one handler) via
build-shared.sh into a real demo3.so, and added api_test.cpp (mirrors
host_test.cpp's dlopen()/dlsym() pattern) confirming jetc_api_demo3
returns the expected two routes at runtime. Re-ran host_test against the
same .so to confirm the render path is unaffected. Confirmed demo.jet/
demo2.jet (no #api lines) emit no JetApiRoute code at all - no regression
for templates that don't declare routes.

Also fixed a gitignore gap surfaced while committing this: an earlier
"jetc/*.cpp" ignore rule (meant for generated *.compiled.cpp output) was
a blanket pattern that silently hid this commit's own api_test.cpp from
git status. Narrowed to "jetc/*.compiled.cpp" (the actual generated-file
naming, per build-compiled.sh/build-shared.sh) and added explicit ignores
for the host_test/api_test/demo3 binaries plus two known stale generated
leftovers (demo.cpp/demo2.cpp) that predate this session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 10:24:10 -07:00

238 lines
11 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) {
for (auto &n : nodes) emitNode(n, indent);
}
void Codegen::emitNode(const NodePtr &n, int indent) {
std::string I = ind(indent);
switch (n->kind) {
case NodeKind::TEXT: {
if (n->text.empty()) break;
out << I << "out << " << cstr(n->text) << ";\n";
break;
}
case NodeKind::VAR: {
out << I << "{ coreutils::ZString v(" << cstr(n->varText) << "); out << scope.getVariable(v, true); }\n";
break;
}
case NodeKind::SET: {
std::string k = id("set_");
out << I << "{\n";
out << I << " scope.keywords[\"" << k << "name\"] = " << cstr(n->setName) << ";\n";
out << I << " scope.keywords[\"" << k << "scope\"] = " << cstr(n->setScope) << ";\n";
out << I << " coreutils::MString val;\n";
if (n->setHasExpr) {
out << I << " scope.keywords[\"" << k << "expr\"] = " << cstr(n->setExpr) << ";\n";
out << I << " val = jet::Operand(scope.keywords[\"" << k << "expr\"], scope).string;\n";
} else {
out << I << " scope.keywords[\"" << k << "value\"] = " << cstr(n->setHasValue ? n->setValue : std::string()) << ";\n";
out << I << " val = scope.resolveKeyword(\"" << k << "value\");\n";
}
out << I << " scope.assignVariable(scope.resolveKeyword(\"" << k << "name\"), val, scope.resolveKeyword(\"" << k << "scope\"));\n";
out << I << "}\n";
break;
}
case NodeKind::IF: {
std::string k = id("if_");
out << I << "{\n";
out << I << " bool cond;\n";
if (n->cond.mode == CondMode::EXPR) {
out << I << " scope.keywords[\"" << k << "expr\"] = " << cstr(n->cond.expr) << ";\n";
out << I << " cond = jet::Operand(scope.keywords[\"" << k << "expr\"], scope).boolean;\n";
} else {
out << I << " scope.keywords[\"" << k << "value1\"] = " << cstr(n->cond.value1) << ";\n";
out << I << " scope.keywords[\"" << k << "value2\"] = " << cstr(n->cond.value2) << ";\n";
out << I << " int rc = scope.resolveKeyword(\"" << k << "value1\").compare(scope.resolveKeyword(\"" << k << "value2\"));\n";
out << I << " cond = (" << staticTypeOp(n->cond.type, "rc") << ");\n";
}
out << I << " if (cond) {\n";
emitBody(n->thenBody, indent + 2);
if (n->hasElse) {
out << I << " } else {\n";
emitBody(n->elseBody, indent + 2);
}
out << I << " }\n";
out << I << "}\n";
break;
}
case NodeKind::FOR: {
std::string k = id("for_");
out << I << "{\n";
out << I << " scope.keywords[\"" << k << "start\"] = " << cstr(n->forHasStart ? n->forStart : std::string("0")) << ";\n";
out << I << " scope.keywords[\"" << k << "end\"] = " << cstr(n->forEnd) << ";\n";
out << I << " scope.keywords[\"" << k << "step\"] = " << cstr(n->forStep) << ";\n";
out << I << " double ix = scope.resolveKeyword(\"" << k << "start\").asDouble();\n";
if (n->forHasName) {
out << I << " scope.keywords[\"" << k << "name\"] = " << cstr(n->forName) << ";\n";
out << I << " scope.keywords[\"" << k << "scope\"] = " << cstr(n->forScope) << ";\n";
}
out << I << " for (; ix <= scope.resolveKeyword(\"" << k << "end\").asDouble(); ix += scope.resolveKeyword(\"" << k << "step\").asDouble()) {\n";
if (n->forHasName) {
out << I << " coreutils::MString nameSel = scope.resolveKeyword(\"" << k << "name\");\n";
out << I << " coreutils::MString scopeSel = scope.resolveKeyword(\"" << k << "scope\");\n";
out << I << " if ((scopeSel == \"\") || (scopeSel == \"global\")) global.variables[nameSel] = ix;\n";
out << I << " else if (scopeSel == \"local\") scope.local->variables[nameSel] = ix;\n";
out << I << " else if (scopeSel == \"parent\") scope.parent->local->variables[nameSel] = ix;\n";
out << I << " else throw coreutils::Exception(\"scope value is not valid.\");\n";
}
emitBody(n->body, indent + 2);
out << I << " }\n";
out << I << "}\n";
break;
}
case NodeKind::WHILE: {
std::string k = id("while_");
out << I << "{\n";
out << I << " bool cond;\n";
if (n->cond.mode == CondMode::EXPR) {
out << I << " scope.keywords[\"" << k << "expr\"] = " << cstr(n->cond.expr) << ";\n";
out << I << " coreutils::MString exprSaved = scope.keywords[\"" << k << "expr\"];\n";
out << I << " cond = jet::Operand(scope.keywords[\"" << k << "expr\"], scope).boolean;\n";
out << I << " while (cond) {\n";
emitBody(n->body, indent + 2);
out << I << " scope.keywords[\"" << k << "expr\"].reset();\n";
out << I << " scope.keywords[\"" << k << "expr\"] = exprSaved;\n";
out << I << " cond = jet::Operand(scope.keywords[\"" << k << "expr\"], scope).boolean;\n";
out << I << " }\n";
} else {
out << I << " scope.keywords[\"" << k << "value1\"] = " << cstr(n->cond.value1) << ";\n";
out << I << " scope.keywords[\"" << k << "value2\"] = " << cstr(n->cond.value2) << ";\n";
out << I << " int rc = scope.resolveKeyword(\"" << k << "value1\").compare(scope.resolveKeyword(\"" << k << "value2\"));\n";
out << I << " cond = (" << staticTypeOp(n->cond.type, "rc") << ");\n";
out << I << " while (cond) {\n";
emitBody(n->body, indent + 2);
out << I << " int rc2 = scope.resolveKeyword(\"" << k << "value1\").compare(scope.resolveKeyword(\"" << k << "value2\"));\n";
out << I << " cond = (" << staticTypeOp(n->cond.type, "rc2") << ");\n";
out << I << " }\n";
}
out << I << "}\n";
break;
}
case NodeKind::FALLBACK: {
out << I << "{ coreutils::ZString span(" << cstr(n->rawSpan) << "); scope.interpretSpan(span, out); }\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();
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";
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.
out << "struct JetApiRoute { const char *method; const char *path; };\n";
out << "extern \"C\" const JetApiRoute *" << apiSymbol << "(int *count) {\n";
out << " static const JetApiRoute routes[] = {\n";
for (auto &r : apiRoutes)
out << " { " << cstr(r.method) << ", " << cstr(r.path) << " },\n";
out << " };\n";
out << " *count = " << apiRoutes.size() << ";\n";
out << " return routes;\n";
out << "}\n\n";
}
out << "extern \"C\" void " << entrySymbol << "(jet::Global &global, coreutils::MString &out) {\n";
out << " coreutils::ZString jetc_empty(\"\");\n";
out << " coreutils::MString jetc_scopeOut;\n";
out << " jet::Tag scope(jetc_empty, jetc_scopeOut, global, NULL, NULL);\n";
emitBody(top, 1);
out << "}\n";
return out.str();
}
out << "static void jetc_render(jet::Global &global, coreutils::MString &out) {\n";
out << " coreutils::ZString jetc_empty(\"\");\n";
out << " coreutils::MString jetc_scopeOut;\n";
out << " jet::Tag scope(jetc_empty, jetc_scopeOut, global, NULL, NULL);\n";
emitBody(top, 1);
out << "}\n\n";
out << "int main(int argc, char **argv, char **envp) {\n";
out << " jet::Global global(envp);\n";
out << " coreutils::MString out;\n";
out << " try {\n";
out << " jetc_render(global, out);\n";
out << " global.outputHeaders();\n";
out << " std::cout << out;\n";
out << " } catch(coreutils::Exception e) {\n";
out << " std::cout << out;\n";
out << " std::cout << \"\\n----- Error: \" << e.text << std::endl;\n";
out << " return 1;\n";
out << " }\n";
out << " return 0;\n";
out << "}\n";
return out.str();
}
}