jetc (JetCore/jetc/) compiles a subset of JET template syntax - literal text, $[...]/#[...] variable output, <set>, <if>/<else>, <for>, <while> - into a standalone C++ program that links against CoreUtils and the same JetCore tag-class library jet-2.0 itself uses. The static tag/text structure becomes real C++ control flow (if/for/while), resolved once at build time instead of being re-scanned character-by-character from raw source on every request and, for loop bodies, on every single iteration (confirmed by reading Tag::parseContainer - it re-tokenizes its container text from scratch every time it runs, since a tag's parse and its execution are the same pass in this codebase). Anything genuinely dynamic - variable values, expr/value comparisons, loop bounds - still calls into the existing runtime (Tag::getVariable/resolveKeyword, jet::Operand) exactly as the interpreter does; compiling doesn't try to eliminate that; only the repeated re-parsing of the template's own shape. A <if type="..."> or <while type="..."> comparison type is resolved to its C++ operator once at compile time (type is a literal attribute value in practice, not runtime data) instead of being re-checked against six string constants on every evaluation. Any tag jetc doesn't have a codegen rule for (mysql, sql, call, include, a <set> with a container body, etc.) is captured as its exact raw source span and handed to the existing interpreter at compiled-program runtime via the new Tag::interpretSpan(), so an unsupported construct degrades gracefully instead of failing to compile - a real, if narrower, program rather than requiring full tag coverage up front. Tag.h/.cpp: made assignVariable public (parallel to the already-public getVariable - generated code needs a write path without being a Tag subclass) and added interpretSpan() as the fallback's entry point into parseContainer. Verified live: two representative templates (jetc/demo.jet, an if/for/set walkthrough; jetc/demo2.jet, a nested <if> inside a <for>, a <while> that mutates and rereads its own condition variable across iterations, and a fallback-interpreted <comment> tag) both compile, link, run standalone, and produce byte-identical output to jet-2.0 running the same source. Building and running jetc/demo2.jet's <while> loop surfaced a real, previously-unknown MString bug (fixed separately, see CoreUtils commit 6e4e463) along with a separate, already-existing bug in __while.cpp's value1/value2/type form (unrelated to this compiler, not fixed here - see project memory). jetc/build-compiled.sh builds the compiler and a given .jet file's compiled binary in one step; jetc/demo.jet and demo2.jet are kept as worked examples. Known limitations: <set> with a container body isn't modeled (falls back to interpretation); a fallback-interpreted tag nested inside compiled control flow uses the template's single top-level scope for parent/local context rather than a faithful per-level chain, which only matters for legacy tags that rely on nested "local" scoping. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
306 lines
10 KiB
C++
Executable File
306 lines
10 KiB
C++
Executable File
#include "JetParser.h"
|
|
#include <cctype>
|
|
#include <stdexcept>
|
|
|
|
namespace jetc {
|
|
|
|
bool Parser::starts(const std::string &lit) const {
|
|
return s.compare(pos, lit.size(), lit) == 0;
|
|
}
|
|
|
|
bool Parser::isNameChar(char c) {
|
|
return std::isalnum((unsigned char)c) || c == '-' || c == '_';
|
|
}
|
|
|
|
void Parser::skipWs() {
|
|
while (!eof() && std::isspace((unsigned char)s[pos])) pos++;
|
|
}
|
|
|
|
std::string Parser::readName() {
|
|
size_t start = pos;
|
|
while (!eof() && isNameChar(s[pos])) pos++;
|
|
return s.substr(start, pos - start);
|
|
}
|
|
|
|
std::string Parser::peekTagName(size_t at) const {
|
|
size_t p = at + 1;
|
|
size_t start = p;
|
|
while (p < s.size() && isNameChar(s[p])) p++;
|
|
return s.substr(start, p - start);
|
|
}
|
|
|
|
bool Parser::matchTagOpen(const std::string &name, size_t at) const {
|
|
if (s.compare(at, 1, "<") != 0) return false;
|
|
size_t p = at + 1;
|
|
size_t nstart = p;
|
|
while (p < s.size() && isNameChar(s[p])) p++;
|
|
if (s.compare(nstart, p - nstart, name) != 0) return false;
|
|
if (p >= s.size()) return false;
|
|
char c = s[p];
|
|
return c == ' ' || c == '>' || c == '/';
|
|
}
|
|
|
|
bool Parser::matchTagClose(const std::string &name, size_t at) const {
|
|
std::string closer = "</" + name + ">";
|
|
return s.compare(at, closer.size(), closer) == 0;
|
|
}
|
|
|
|
bool Parser::skipOpenTag(size_t &p) const {
|
|
p++; // '<'
|
|
while (p < s.size() && isNameChar(s[p])) p++;
|
|
while (p < s.size()) {
|
|
if (s[p] == '"') {
|
|
p++;
|
|
while (p < s.size() && s[p] != '"') {
|
|
if (s[p] == '\\' && p + 1 < s.size()) p++;
|
|
p++;
|
|
}
|
|
if (p < s.size()) p++;
|
|
continue;
|
|
}
|
|
if (s.compare(p, 2, "/>") == 0) { p += 2; return true; }
|
|
if (s[p] == '>') { p += 1; return false; }
|
|
p++;
|
|
}
|
|
throw std::runtime_error("jetc: unterminated tag near end of input");
|
|
}
|
|
|
|
std::map<std::string, std::string> Parser::parseAttrs(bool &hasContainer) {
|
|
std::map<std::string, std::string> attrs;
|
|
while (true) {
|
|
skipWs();
|
|
if (eof()) throw std::runtime_error("jetc: unterminated tag near end of input");
|
|
if (starts("/>")) { pos += 2; hasContainer = false; break; }
|
|
if (starts(">")) { pos += 1; hasContainer = true; break; }
|
|
std::string aname = readName();
|
|
if (aname.empty()) throw std::runtime_error("jetc: malformed attribute near offset " + std::to_string(pos));
|
|
if (starts("=\"")) {
|
|
pos += 2;
|
|
std::string val;
|
|
while (!eof() && s[pos] != '"') {
|
|
if (s[pos] == '\\' && pos + 1 < s.size()) { val += s[pos]; val += s[pos + 1]; pos += 2; continue; }
|
|
val += s[pos++];
|
|
}
|
|
if (!eof()) pos++; // closing quote
|
|
attrs[aname] = val;
|
|
}
|
|
}
|
|
return attrs;
|
|
}
|
|
|
|
std::pair<size_t, size_t> Parser::scanBody(size_t bodyStart, const std::string &tagName, bool lookForElse) {
|
|
size_t p = bodyStart;
|
|
int depth = 1;
|
|
size_t elsePos = std::string::npos;
|
|
while (p < s.size()) {
|
|
if (matchTagClose(tagName, p)) {
|
|
depth--;
|
|
if (depth == 0) return {p, elsePos};
|
|
p += 3 + tagName.size();
|
|
continue;
|
|
}
|
|
if (lookForElse && depth == 1 && elsePos == std::string::npos && s.compare(p, 6, "<else>") == 0) {
|
|
elsePos = p;
|
|
p += 6;
|
|
continue;
|
|
}
|
|
if (matchTagOpen(tagName, p)) {
|
|
bool selfClosing = skipOpenTag(p);
|
|
if (!selfClosing) depth++;
|
|
continue;
|
|
}
|
|
if (s[p] == '<') {
|
|
if (s.compare(p, 2, "</") == 0) {
|
|
size_t q = p + 2;
|
|
while (q < s.size() && isNameChar(s[q])) q++;
|
|
if (q < s.size() && s[q] == '>') q++;
|
|
p = q;
|
|
} else {
|
|
skipOpenTag(p);
|
|
}
|
|
continue;
|
|
}
|
|
p++;
|
|
}
|
|
throw std::runtime_error("jetc: unterminated <" + tagName + "> - no matching close tag found");
|
|
}
|
|
|
|
std::vector<NodePtr> Parser::parseSub(size_t start, size_t end) {
|
|
Parser sub(s.substr(start, end - start));
|
|
return sub.parseNodes();
|
|
}
|
|
|
|
NodePtr Parser::parseVar() {
|
|
size_t start = pos;
|
|
pos += 2; // "$[" or "#["
|
|
int depth = 1;
|
|
while (!eof() && depth > 0) {
|
|
if (starts("$[") || starts("#[")) { depth++; pos += 2; continue; }
|
|
if (s[pos] == ']') { depth--; pos++; continue; }
|
|
pos++;
|
|
}
|
|
auto n = std::make_shared<Node>();
|
|
n->kind = NodeKind::VAR;
|
|
n->varText = s.substr(start, pos - start);
|
|
return n;
|
|
}
|
|
|
|
NodePtr Parser::parseSet() {
|
|
size_t origStart = pos;
|
|
pos += 4; // "<set"
|
|
bool hasContainer;
|
|
auto attrs = parseAttrs(hasContainer);
|
|
if (hasContainer) {
|
|
// Container-form <set> (value comes from rendering nested content)
|
|
// isn't modeled by this prototype - fall back to full interpretation
|
|
// of the whole tag instead of failing to compile.
|
|
auto span = scanBody(pos, "set", false);
|
|
pos = span.first + 3 + 3; // "</set>"
|
|
auto n = std::make_shared<Node>();
|
|
n->kind = NodeKind::FALLBACK;
|
|
n->rawSpan = s.substr(origStart, pos - origStart);
|
|
return n;
|
|
}
|
|
auto n = std::make_shared<Node>();
|
|
n->kind = NodeKind::SET;
|
|
n->setName = attrs.count("name") ? attrs["name"] : "";
|
|
if (attrs.count("value")) { n->setHasValue = true; n->setValue = attrs["value"]; }
|
|
if (attrs.count("expr")) { n->setHasExpr = true; n->setExpr = attrs["expr"]; }
|
|
n->setScope = attrs.count("scope") ? attrs["scope"] : "";
|
|
return n;
|
|
}
|
|
|
|
NodePtr Parser::parseIf() {
|
|
pos += 3; // "<if"
|
|
bool hasContainer;
|
|
auto attrs = parseAttrs(hasContainer);
|
|
auto n = std::make_shared<Node>();
|
|
n->kind = NodeKind::IF;
|
|
if (attrs.count("expr")) {
|
|
n->cond.mode = CondMode::EXPR;
|
|
n->cond.expr = attrs["expr"];
|
|
} else {
|
|
n->cond.mode = CondMode::VALUES;
|
|
n->cond.value1 = attrs.count("value1") ? attrs["value1"] : "";
|
|
n->cond.value2 = attrs.count("value2") ? attrs["value2"] : "";
|
|
n->cond.type = attrs.count("type") ? attrs["type"] : "";
|
|
}
|
|
if (hasContainer) {
|
|
auto span = scanBody(pos, "if", true);
|
|
size_t closePos = span.first, elsePos = span.second;
|
|
size_t thenEnd = (elsePos != std::string::npos) ? elsePos : closePos;
|
|
n->thenBody = parseSub(pos, thenEnd);
|
|
if (elsePos != std::string::npos) {
|
|
n->hasElse = true;
|
|
n->elseBody = parseSub(elsePos + 6, closePos);
|
|
}
|
|
pos = closePos + 3 + 2; // "</if>"
|
|
}
|
|
return n;
|
|
}
|
|
|
|
NodePtr Parser::parseFor() {
|
|
pos += 4; // "<for"
|
|
bool hasContainer;
|
|
auto attrs = parseAttrs(hasContainer);
|
|
auto n = std::make_shared<Node>();
|
|
n->kind = NodeKind::FOR;
|
|
if (attrs.count("start")) { n->forHasStart = true; n->forStart = attrs["start"]; }
|
|
n->forEnd = attrs.count("end") ? attrs["end"] : "0";
|
|
n->forStep = attrs.count("step") ? attrs["step"] : "1";
|
|
if (attrs.count("name")) { n->forHasName = true; n->forName = attrs["name"]; }
|
|
n->forScope = attrs.count("scope") ? attrs["scope"] : "";
|
|
if (hasContainer) {
|
|
auto span = scanBody(pos, "for", false);
|
|
n->body = parseSub(pos, span.first);
|
|
pos = span.first + 3 + 3; // "</for>"
|
|
}
|
|
return n;
|
|
}
|
|
|
|
NodePtr Parser::parseWhile() {
|
|
pos += 6; // "<while"
|
|
bool hasContainer;
|
|
auto attrs = parseAttrs(hasContainer);
|
|
auto n = std::make_shared<Node>();
|
|
n->kind = NodeKind::WHILE;
|
|
if (attrs.count("expr")) {
|
|
n->cond.mode = CondMode::EXPR;
|
|
n->cond.expr = attrs["expr"];
|
|
} else {
|
|
n->cond.mode = CondMode::VALUES;
|
|
n->cond.value1 = attrs.count("value1") ? attrs["value1"] : "";
|
|
n->cond.value2 = attrs.count("value2") ? attrs["value2"] : "";
|
|
n->cond.type = attrs.count("type") ? attrs["type"] : "";
|
|
}
|
|
if (hasContainer) {
|
|
auto span = scanBody(pos, "while", false);
|
|
n->body = parseSub(pos, span.first);
|
|
pos = span.first + 3 + 5; // "</while>"
|
|
}
|
|
return n;
|
|
}
|
|
|
|
NodePtr Parser::parseFallback() {
|
|
size_t origStart = pos;
|
|
std::string tagName = peekTagName(pos);
|
|
size_t p = pos;
|
|
bool selfClosing = skipOpenTag(p);
|
|
if (!selfClosing) {
|
|
auto span = scanBody(p, tagName, false);
|
|
p = span.first + 3 + tagName.size();
|
|
}
|
|
pos = p;
|
|
auto n = std::make_shared<Node>();
|
|
n->kind = NodeKind::FALLBACK;
|
|
n->rawSpan = s.substr(origStart, pos - origStart);
|
|
return n;
|
|
}
|
|
|
|
std::vector<NodePtr> Parser::parseNodes() {
|
|
std::vector<NodePtr> nodes;
|
|
std::string textBuf;
|
|
auto flush = [&]() {
|
|
if (!textBuf.empty()) {
|
|
auto n = std::make_shared<Node>();
|
|
n->kind = NodeKind::TEXT;
|
|
n->text = textBuf;
|
|
nodes.push_back(n);
|
|
textBuf.clear();
|
|
}
|
|
};
|
|
while (!eof()) {
|
|
if (starts("|>")) {
|
|
flush();
|
|
pos += 2;
|
|
std::string buf;
|
|
while (!eof() && !starts("<|")) buf += s[pos++];
|
|
if (!eof()) pos += 2;
|
|
auto n = std::make_shared<Node>();
|
|
n->kind = NodeKind::TEXT;
|
|
n->text = buf;
|
|
nodes.push_back(n);
|
|
continue;
|
|
}
|
|
if (s[pos] == '<') {
|
|
if (matchTagOpen("set", pos)) { flush(); nodes.push_back(parseSet()); continue; }
|
|
if (matchTagOpen("if", pos)) { flush(); nodes.push_back(parseIf()); continue; }
|
|
if (matchTagOpen("for", pos)) { flush(); nodes.push_back(parseFor()); continue; }
|
|
if (matchTagOpen("while", pos)) { flush(); nodes.push_back(parseWhile()); continue; }
|
|
flush();
|
|
nodes.push_back(parseFallback());
|
|
continue;
|
|
}
|
|
if (starts("$[") || starts("#[")) {
|
|
flush();
|
|
nodes.push_back(parseVar());
|
|
continue;
|
|
}
|
|
textBuf += s[pos++];
|
|
}
|
|
flush();
|
|
return nodes;
|
|
}
|
|
|
|
}
|