Files
JetCore/jetc/JetParser.cpp
T
Brad ArantandClaude Sonnet 5 d52ca61250 Add jetc: a prototype JET-to-C++ compiler, plus the Tag API it needed
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>
2026-08-23 19:18:41 -07:00

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;
}
}