XPlor/libs/dsl/interpreter.cpp

803 lines
29 KiB
C++
Raw Normal View History

2026-01-01 22:18:25 -05:00
#include "interpreter.h"
#include "compression.h"
#include "utils.h"
#include "logmanager.h"
#include <stdexcept>
#include <QFileInfo>
Interpreter::Interpreter(Module mod) : m_mod(std::move(mod)) {}
static QDataStream::ByteOrder toQtOrder(ByteOrder b) {
return (b == ByteOrder::LE) ? QDataStream::LittleEndian : QDataStream::BigEndian;
}
QVariantMap Interpreter::runType(const QString& typeName, QIODevice* dev, const QString &filePath) const {
QDataStream ds(dev);
return runType(typeName, ds, filePath);
}
QVariantMap Interpreter::runType(const QString& typeName, QDataStream& stream, const QString& filePath) const {
if (!m_mod.types.contains(typeName)) {
throw std::runtime_error(("Unknown type: " + typeName).toStdString());
}
const qint64 startPos = stream.device() ? stream.device()->pos() : -1;
LogManager::instance().addEntry(QString("[PARSE] Starting type '%1' at pos 0x%2")
.arg(typeName)
.arg(startPos, 0, 16));
Runtime rt;
rt.in = &stream;
rt.module = &m_mod;
const TypeDef& td = m_mod.types[typeName];
rt.order = td.order;
applyByteOrder(rt);
if (!filePath.isEmpty())
{
seedFileVars(rt, filePath);
}
execBlock(rt, td.body);
rt.vars["_type"] = typeName;
const qint64 endPos = stream.device() ? stream.device()->pos() : -1;
LogManager::instance().addEntry(QString("[PARSE] Finished type '%1' at pos 0x%2 (consumed %3 bytes)")
.arg(typeName)
.arg(endPos, 0, 16)
.arg(endPos - startPos));
return rt.vars;
}
void Interpreter::seedFileVars(Runtime& rt, const QString& filePath) {
rt.filePath = filePath;
QFileInfo fi(filePath);
rt.vars["_path"] = filePath;
rt.vars["_name"] = fi.fileName(); // "mp_test_load.ff"
rt.vars["_basename"] = fi.completeBaseName(); // "mp_test_load" (without extension)
rt.vars["_ext"] = fi.suffix().toLower(); // "ff", "xpak", etc.
}
bool Interpreter::checkCriteria(const QString &typeName, QIODevice *dev, const QString filePath) const
{
if (!m_mod.types.contains(typeName))
{
LogManager::instance().addEntry(QString("[CRITERIA] Type '%1' not found").arg(typeName));
return false;
}
const TypeDef& td = m_mod.types[typeName];
if (td.criteria.isEmpty())
{
LogManager::instance().addEntry(QString("[CRITERIA] Type '%1' has no criteria, returning true").arg(typeName));
return true;
}
LogManager::instance().addEntry(QString("[CRITERIA] Checking type '%1' for file '%2'").arg(typeName).arg(filePath));
// Save/restore device position
const qint64 savedPos = dev->pos();
auto restore = [&]() { dev->seek(savedPos); };
try {
QDataStream ds(dev);
Runtime rt;
rt.in = &ds;
rt.module = &m_mod;
rt.order = td.order;
applyByteOrder(rt);
if (!filePath.isEmpty())
{
seedFileVars(rt, filePath);
}
execBlock(rt, td.criteria);
restore();
LogManager::instance().addEntry(QString("[CRITERIA] Type '%1' PASSED criteria").arg(typeName));
return true;
} catch (const std::exception& e) {
restore();
LogManager::instance().addEntry(QString("[CRITERIA] Type '%1' FAILED: %2").arg(typeName).arg(e.what()));
return false;
} catch (...) {
restore();
LogManager::instance().addEntry(QString("[CRITERIA] Type '%1' FAILED (unknown exception)").arg(typeName));
return false;
}
}
QVariantMap Interpreter::runTypeInternal(const QString &typeName, QDataStream &stream, const QString &filePath, std::optional<ByteOrder> inheritedOrder) const
{
if (!m_mod.types.contains(typeName)) {
throw std::runtime_error(("Unknown type: " + typeName).toStdString());
}
Runtime rt;
rt.in = &stream;
rt.module = &m_mod;
const TypeDef& td = m_mod.types[typeName];
// IMPORTANT: inherit if the typedef doesn't specify byteorder
if (td.hasExplicitByteOrder) rt.order = td.order;
else if (inheritedOrder.has_value()) rt.order = *inheritedOrder;
else rt.order = td.order; // root fallback (keeps old behavior)
applyByteOrder(rt);
if (!filePath.isEmpty()) seedFileVars(rt, filePath);
execBlock(rt, td.body);
rt.vars["_type"] = typeName;
return rt.vars;
}
void Interpreter::applyByteOrder(Runtime& rt) const {
if (!rt.in) return;
rt.in->setByteOrder(toQtOrder(rt.order));
}
qint64 Interpreter::toInt(const QVariant& v) const {
if (v.canConvert<qint64>()) return v.toLongLong();
throw std::runtime_error("Expected integer value");
}
bool Interpreter::toBool(const QVariant& v) const {
if (v.typeId() == QMetaType::Bool) return v.toBool();
if (v.canConvert<qint64>()) return v.toLongLong() != 0;
if (v.typeId() == QMetaType::QString) return !v.toString().isEmpty();
if (v.canConvert<QByteArray>()) return !v.toByteArray().isEmpty();
return v.isValid();
}
QVariant Interpreter::readScalar(Runtime& rt, ScalarType t) const {
QDataStream& in = *rt.in;
switch (t) {
case ScalarType::U8: { quint8 v; in >> v; return v; }
case ScalarType::I8: { qint8 v; in >> v; return v; }
case ScalarType::U16: { quint16 v; in >> v; return v; }
case ScalarType::I16: { qint16 v; in >> v; return v; }
case ScalarType::U32: { quint32 v; in >> v; return v; }
case ScalarType::I32: { qint32 v; in >> v; return v; }
case ScalarType::U64: { quint64 v; in >> v; return v; }
case ScalarType::I64: { qint64 v; in >> v; return v; }
case ScalarType::Bool: { quint8 v; in >> v; return (v != 0); }
}
return {};
}
QByteArray Interpreter::readBytes(Runtime& rt, qint64 n) const {
if (n < 0) throw std::runtime_error("read(n): n must be >= 0");
QIODevice* dev = rt.in->device();
if (!dev) throw std::runtime_error("No device in stream");
QByteArray buf;
buf.resize(int(n));
const int got = rt.in->readRawData(buf.data(), int(n));
if (got != int(n)) {
buf.resize(qMax(0, got));
throw std::runtime_error("Unexpected EOF while reading bytes");
}
return buf;
}
QByteArray Interpreter::readEOF(Runtime& rt) const {
QIODevice* dev = rt.in->device();
if (!dev) throw std::runtime_error("No device in stream");
const qint64 remaining = dev->size() - dev->pos();
if (remaining < 0) return {};
return readBytes(rt, remaining);
}
QVariant Interpreter::evalCall(Runtime& rt, const Expr::Call& c) const {
// Builtins:
// pos() -> int
// size() -> int
// read(n) where n is int or EOF -> bytes
// zlib(bytes) -> bytes
// parse("typeName", bytes) -> map
if (c.fn == "pos") {
if (!c.args.isEmpty()) throw std::runtime_error("pos() takes no args");
return rt.in->device()->pos();
}
if (c.fn == "size") {
if (!c.args.isEmpty()) throw std::runtime_error("size() takes no args");
return rt.in->device()->size();
}
if (c.fn == "read") {
if (c.args.size() != 1) throw std::runtime_error("read(x) takes 1 arg");
QVariant arg = evalExpr(rt, *c.args[0]);
// special: EOF token was represented as Var("EOF")
if (arg.typeId() == QMetaType::QString && arg.toString() == "EOF") {
return readEOF(rt);
}
return readBytes(rt, toInt(arg));
}
if (c.fn == "zlib") {
if (c.args.size() != 1) throw std::runtime_error("zlib(bytes) takes 1 arg");
QByteArray in = evalExpr(rt, *c.args[0]).toByteArray();
const QByteArray decompressed = Compression::DecompressZLIB(in);
Utils::ExportData(rt.filePath.split('/').last() + ".zone", decompressed);
if (!rt.filePath.isEmpty()) {
QFileInfo fi(rt.filePath);
const QString stem = fi.completeBaseName(); // "mp_test_load"
rt.vars["_zone_name"] = stem + ".zone"; // "mp_test_load.zone"
}
return decompressed;
}
if (c.fn == "parse") {
if (c.args.size() != 2) throw std::runtime_error("parse(typeName, bytes) takes 2 args");
const QString typeName = evalExpr(rt, *c.args[0]).toString();
const QByteArray bytes = evalExpr(rt, *c.args[1]).toByteArray();
QDataStream nested(bytes);
QVariantMap obj = runTypeInternal(typeName, nested, rt.filePath, rt.order);
if (rt.vars.contains("_zone_name") && !obj.contains("_zone_name")) {
obj["_zone_name"] = rt.vars["_zone_name"];
}
obj["_type"] = typeName;
return obj;
}
if (c.fn == "parse_here") {
if (c.args.size() != 1) throw std::runtime_error("parse_here(typeName) takes 1 arg");
const QString typeName = evalExpr(rt, *c.args[0]).toString();
if (!rt.module->types.contains(typeName)) {
throw std::runtime_error(("Unknown type (parse_here): " + typeName).toStdString());
}
const qint64 startPos = rt.in->device() ? rt.in->device()->pos() : -1;
LogManager::instance().addEntry(QString("[PARSE_HERE] Parsing '%1' at pos 0x%2")
.arg(typeName)
.arg(startPos, 0, 16));
// IMPORTANT: parse using the SAME stream (advance)
// We must temporarily apply that type's byteorder.
const TypeDef& td = rt.module->types[typeName];
// Save current byteorder
const auto oldOrder = rt.order;
// Decide child order
ByteOrder childOrder = oldOrder;
if (td.hasExplicitByteOrder) childOrder = td.order;
// Create child runtime sharing stream
Runtime childRt;
childRt.in = rt.in;
childRt.module = rt.module;
childRt.order = childOrder;
applyByteOrder(childRt);
execBlock(childRt, td.body);
QVariantMap child = childRt.vars;
// Restore parent order
rt.order = oldOrder;
applyByteOrder(rt);
child["_type"] = typeName;
const qint64 endPos = rt.in->device() ? rt.in->device()->pos() : -1;
LogManager::instance().addEntry(QString("[PARSE_HERE] Finished '%1' at pos 0x%2 (consumed %3 bytes)")
.arg(typeName)
.arg(endPos, 0, 16)
.arg(endPos - startPos));
return child;
}
if (c.fn == "push") {
if (c.args.size() != 2) throw std::runtime_error("push(name, value) takes 2 args");
const QString listName = evalExpr(rt, *c.args[0]).toString();
QVariant v = evalExpr(rt, *c.args[1]);
if (v.typeId() != QMetaType::QVariantMap)
throw std::runtime_error("push expects a map as 2nd arg");
QVariantList lst = rt.vars.value(listName).toList(); // empty if missing/not a list
const int newIndex = lst.size();
lst.push_back(v);
rt.vars[listName] = lst;
const QVariantMap itemMap = v.toMap();
const QString itemType = itemMap.value("_type").toString();
const QString itemName = itemMap.value("_name").toString();
LogManager::instance().addEntry(QString("[PUSH] Added item #%1 to '%2': type='%3', name='%4'")
.arg(newIndex)
.arg(listName)
.arg(itemType)
.arg(itemName));
return rt.vars[listName];
}
if (c.fn == "get") {
if (c.args.size() != 2) throw std::runtime_error("get(container, key) takes 2 args");
QVariant container = evalExpr(rt, *c.args[0]);
QVariant key = evalExpr(rt, *c.args[1]);
// Array indexing: get(array, index)
if (container.typeId() == QMetaType::QVariantList) {
const QVariantList list = container.toList();
const qint64 index = toInt(key);
if (index < 0 || index >= list.size()) {
throw std::runtime_error(QString("get: array index %1 out of bounds (size=%2)")
.arg(index).arg(list.size()).toStdString());
}
return list[int(index)];
}
// Object field access: get(object, "fieldName")
if (container.typeId() == QMetaType::QVariantMap) {
const QVariantMap map = container.toMap();
const QString fieldName = key.toString();
if (!map.contains(fieldName)) {
throw std::runtime_error(QString("get: field '%1' not found in object")
.arg(fieldName).toStdString());
}
return map.value(fieldName);
}
throw std::runtime_error("get: first argument must be an array or object");
}
if (c.fn == "set") {
if (c.args.size() != 3) throw std::runtime_error("set(varName, key, value) takes 3 args");
const QString varName = evalExpr(rt, *c.args[0]).toString();
QVariant key = evalExpr(rt, *c.args[1]);
QVariant value = evalExpr(rt, *c.args[2]);
if (!rt.vars.contains(varName)) {
throw std::runtime_error(QString("set: variable '%1' not found").arg(varName).toStdString());
}
QVariant container = rt.vars[varName];
// Array indexing: set("arrayVar", index, value)
if (container.typeId() == QMetaType::QVariantList) {
QVariantList list = container.toList();
const qint64 index = toInt(key);
if (index < 0 || index >= list.size()) {
throw std::runtime_error(QString("set: array index %1 out of bounds (size=%2)")
.arg(index).arg(list.size()).toStdString());
}
list[int(index)] = value;
rt.vars[varName] = list;
return value;
}
// Object field access: set("objectVar", "fieldName", value)
if (container.typeId() == QMetaType::QVariantMap) {
QVariantMap map = container.toMap();
const QString fieldName = key.toString();
map[fieldName] = value;
rt.vars[varName] = map;
return value;
}
throw std::runtime_error("set: variable must be an array or object");
}
auto readAt = [&](qint64 offset, int nbytes)->QByteArray {
QIODevice* dev = rt.in->device();
if (!dev) throw std::runtime_error("No device");
if (offset < 0) throw std::runtime_error("readAt: offset must be >= 0");
if (nbytes < 0) throw std::runtime_error("readAt: nbytes must be >= 0");
const qint64 saved = dev->pos();
if (!dev->seek(offset)) throw std::runtime_error("seek failed");
QByteArray buf(nbytes, Qt::Uninitialized);
const int got = rt.in->readRawData(buf.data(), nbytes);
dev->seek(saved);
if (got != nbytes) throw std::runtime_error("readAt short read");
return buf;
};
// u8at(offset)
if (c.fn == "u8at") {
if (c.args.size() != 1) throw std::runtime_error("u8at(off) takes 1 arg");
qint64 off = toInt(evalExpr(rt, *c.args[0]));
QByteArray b = readAt(off, 1);
return quint8(b[0]);
}
if (c.fn == "u16at") {
if (c.args.size() != 1) throw std::runtime_error("u16at(off) takes 1 arg");
qint64 off = toInt(evalExpr(rt, *c.args[0]));
QByteArray b = readAt(off, 2);
const quint8 b0 = quint8(b[0]), b1 = quint8(b[1]);
if (rt.order == ByteOrder::LE) return quint16(b0 | (b1 << 8));
return quint16((b0 << 8) | b1);
}
if (c.fn == "u32at") {
if (c.args.size() != 1) throw std::runtime_error("u32at(off) takes 1 arg");
qint64 off = toInt(evalExpr(rt, *c.args[0]));
QByteArray b = readAt(off, 4);
quint32 v = 0;
if (rt.order == ByteOrder::LE) {
v = quint32(quint8(b[0])) |
(quint32(quint8(b[1])) << 8) |
(quint32(quint8(b[2])) << 16) |
(quint32(quint8(b[3])) << 24);
} else {
v = (quint32(quint8(b[0])) << 24) |
(quint32(quint8(b[1])) << 16) |
(quint32(quint8(b[2])) << 8) |
quint32(quint8(b[3]));
}
return v;
}
if (c.fn == "u64at") {
if (c.args.size() != 1) throw std::runtime_error("u64at(off) takes 1 arg");
qint64 off = toInt(evalExpr(rt, *c.args[0]));
QByteArray b = readAt(off, 8);
quint64 v = 0;
if (rt.order == ByteOrder::LE) {
for (int i = 0; i < 8; ++i) v |= (quint64(quint8(b[i])) << (8 * i));
} else {
for (int i = 0; i < 8; ++i) v = (v << 8) | quint8(b[i]);
}
return v;
}
// optional helper for magic checks
if (c.fn == "bytesat") {
if (c.args.size() != 2) throw std::runtime_error("bytesat(off,len) takes 2 args");
qint64 off = toInt(evalExpr(rt, *c.args[0]));
int len = int(toInt(evalExpr(rt, *c.args[1])));
return readAt(off, len);
}
if (c.fn == "ascii") {
if (c.args.size() != 1) throw std::runtime_error("ascii(bytes) takes 1 arg");
const QByteArray b = evalExpr(rt, *c.args[0]).toByteArray();
return QString::fromLatin1(b); // strict 1:1 byte->char (good for magic tags)
}
if (c.fn == "cstring") {
if (c.args.size() != 0) throw std::runtime_error("cstring() takes 0 args");
// Read null-terminated string (C-style string)
QByteArray result;
char ch;
while (true) {
if (rt.in->readRawData(&ch, 1) != 1) {
throw std::runtime_error("cstring(): unexpected EOF");
}
if (ch == '\0') break;
result.append(ch);
}
return QString::fromUtf8(result);
}
if (c.fn == "set_global") {
if (c.args.size() != 2) throw std::runtime_error("set_global(name, value) takes 2 args");
const QString name = evalExpr(rt, *c.args[0]).toString();
const QVariant value = evalExpr(rt, *c.args[1]);
rt.module->globals[name] = value;
return value;
}
if (c.fn == "write_file") {
if (c.args.size() != 2) throw std::runtime_error("write_file(filename, data) takes 2 args");
const QString filename = evalExpr(rt, *c.args[0]).toString();
const QByteArray data = evalExpr(rt, *c.args[1]).toByteArray();
// Use Utils::ExportData which writes to exports/ directory
if (!Utils::ExportData(filename, data)) {
throw std::runtime_error(("Failed to export file: " + filename).toStdString());
}
LogManager::instance().addEntry(QString("[EXPORT] Exported %1 bytes to exports/%2")
.arg(data.size())
.arg(filename));
return data.size(); // Return number of bytes written
}
throw std::runtime_error(("Unknown function: " + c.fn).toStdString());
}
QVariant Interpreter::evalPipe(Runtime& rt, const Expr::Pipe& p) const {
QVariant v = evalExpr(rt, *p.base);
for (const auto& stage : p.stages) {
if (stage.fn == "zlib") {
if (!v.canConvert<QByteArray>()) throw std::runtime_error("zlib stage expects bytes");
const QByteArray in = v.toByteArray();
const QByteArray decompressed = Compression::DecompressZLIB(in);
// Match evalCall("zlib") behavior
if (!rt.filePath.isEmpty()) {
QFileInfo fi(rt.filePath);
const QString stem = fi.completeBaseName(); // "mp_test_load"
rt.vars["_zone_name"] = stem + ".zone"; // "mp_test_load.zone"
}
v = decompressed;
continue;
}
if (stage.fn == "parse") {
// stage args[0] is a String expr holding type name (we created it that way in parser)
if (stage.args.size() != 1) throw std::runtime_error("parse stage requires type name");
const QString typeName = stage.args[0]->node.index() == 1
? std::get<Expr::String>(stage.args[0]->node).v
: evalExpr(rt, *stage.args[0]).toString();
if (!v.canConvert<QByteArray>()) throw std::runtime_error("parse stage expects bytes");
const QByteArray bytes = v.toByteArray();
QDataStream nested(bytes);
QVariantMap obj = runTypeInternal(typeName, nested, rt.filePath, rt.order);
// propagate zone label just like evalCall("parse")
if (rt.vars.contains("_zone_name") && !obj.contains("_zone_name")) {
obj["_zone_name"] = rt.vars["_zone_name"];
}
obj["_type"] = typeName;
v = obj;
continue;
}
// Allow calling a stage as fn(current) for future extensions
// e.g. stage "zlib" already handled above
throw std::runtime_error(("Unknown pipeline stage: " + stage.fn).toStdString());
}
return v;
}
QVariant Interpreter::evalExpr(Runtime& rt, const Expr& e) const {
if (std::holds_alternative<Expr::Int>(e.node)) {
return std::get<Expr::Int>(e.node).v;
}
if (std::holds_alternative<Expr::String>(e.node)) {
return std::get<Expr::String>(e.node).v;
}
if (std::holds_alternative<Expr::Var>(e.node)) {
const QString name = std::get<Expr::Var>(e.node).name;
// EOF is a sentinel used only inside read(...)
if (name == "EOF") return QString("EOF");
// Check local vars first, then globals
if (rt.vars.contains(name)) {
return rt.vars.value(name);
}
if (rt.module && rt.module->globals.contains(name)) {
return rt.module->globals.value(name);
}
throw std::runtime_error(("Unknown variable: " + name).toStdString());
}
if (std::holds_alternative<Expr::Unary>(e.node)) {
const auto& u = std::get<Expr::Unary>(e.node);
QVariant rhs = evalExpr(rt, *u.rhs);
if (u.op == "!") return !toBool(rhs);
if (u.op == "-") return -toInt(rhs);
if (u.op == "+") return +toInt(rhs);
throw std::runtime_error(("Unknown unary op: " + u.op).toStdString());
}
if (std::holds_alternative<Expr::Binary>(e.node)) {
const auto& b = std::get<Expr::Binary>(e.node);
QVariant lv = evalExpr(rt, *b.lhs);
QVariant rv = evalExpr(rt, *b.rhs);
const QString& op = b.op;
// comparisons/logical
if (op == "==") return lv == rv;
if (op == "!=") return lv != rv;
if (op == "<") return toInt(lv) < toInt(rv);
if (op == "<=") return toInt(lv) <= toInt(rv);
if (op == ">") return toInt(lv) > toInt(rv);
if (op == ">=") return toInt(lv) >= toInt(rv);
if (op == "&&") return toBool(lv) && toBool(rv);
if (op == "||") return toBool(lv) || toBool(rv);
// Handle string concatenation for +
if (op == "+") {
// If either operand is a string, do string concatenation
if (lv.typeId() == QMetaType::QString || rv.typeId() == QMetaType::QString) {
return lv.toString() + rv.toString();
}
// Otherwise do integer addition
return toInt(lv) + toInt(rv);
}
// arithmetic/bitwise
const qint64 a = toInt(lv);
const qint64 c = toInt(rv);
if (op == "-") return a - c;
if (op == "*") return a * c;
if (op == "/") return c == 0 ? 0 : (a / c);
if (op == "%") return c == 0 ? 0 : (a % c);
if (op == "<<") return a << c;
if (op == ">>") return a >> c;
if (op == "&") return a & c;
if (op == "^") return a ^ c;
if (op == "|") return a | c;
throw std::runtime_error(("Unknown binary op: " + op).toStdString());
}
if (std::holds_alternative<Expr::Call>(e.node)) {
return evalCall(rt, std::get<Expr::Call>(e.node));
}
if (std::holds_alternative<Expr::Pipe>(e.node)) {
return evalPipe(rt, std::get<Expr::Pipe>(e.node));
}
throw std::runtime_error("Unknown expression node");
}
void Interpreter::execBlock(Runtime& rt, const QVector<StmtPtr>& body) const {
for (const auto& s : body) execStmt(rt, *s);
}
void Interpreter::execStmt(Runtime& rt, const Stmt& s) const {
if (std::holds_alternative<Stmt::ReadScalar>(s.node)) {
const auto& rs = std::get<Stmt::ReadScalar>(s.node);
QVariant v = readScalar(rt, rs.type);
rt.vars[rs.name] = v;
return;
}
if (std::holds_alternative<Stmt::Skip>(s.node)) {
const auto& sk = std::get<Stmt::Skip>(s.node);
const qint64 n = toInt(evalExpr(rt, *sk.count));
if (n < 0) throw std::runtime_error("skip n must be >= 0");
const int skipped = rt.in->skipRawData(int(n));
if (skipped != int(n)) throw std::runtime_error("Unexpected EOF during skip");
return;
}
if (std::holds_alternative<Stmt::Align>(s.node)) {
const auto& al = std::get<Stmt::Align>(s.node);
const qint64 n = toInt(evalExpr(rt, *al.n));
if (n <= 0) throw std::runtime_error("align(n): n must be > 0");
QIODevice* dev = rt.in->device();
const qint64 pos = dev->pos();
const qint64 mod = pos % n;
if (mod != 0) {
const qint64 pad = n - mod;
const int skipped = rt.in->skipRawData(int(pad));
if (skipped != int(pad)) throw std::runtime_error("Unexpected EOF during align");
}
return;
}
if (std::holds_alternative<Stmt::Seek>(s.node)) {
const auto& se = std::get<Stmt::Seek>(s.node);
const qint64 p = toInt(evalExpr(rt, *se.pos));
if (p < 0) throw std::runtime_error("seek(pos): position must be >= 0");
QIODevice* dev = rt.in->device();
if (!dev->seek(p)) throw std::runtime_error("seek(pos) failed");
return;
}
if (std::holds_alternative<Stmt::Assign>(s.node)) {
const auto& as = std::get<Stmt::Assign>(s.node);
QVariant v = evalExpr(rt, *as.value);
rt.vars[as.name] = v;
// Log assignments of key fields for debugging
if (as.name == "_name" || as.name == "asset_type" || as.name == "_type") {
LogManager::instance().addEntry(QString("[ASSIGN] %1 = '%2'")
.arg(as.name)
.arg(v.toString()));
}
// Store hidden flag if set
if (as.ui.hidden) {
rt.vars["_hidden"] = true;
}
return;
}
if (std::holds_alternative<Stmt::If>(s.node)) {
const auto& iff = std::get<Stmt::If>(s.node);
const bool cond = toBool(evalExpr(rt, *iff.cond));
execBlock(rt, cond ? iff.thenBody : iff.elseBody);
return;
}
if (std::holds_alternative<Stmt::While>(s.node)) {
const auto& wh = std::get<Stmt::While>(s.node);
while (toBool(evalExpr(rt, *wh.cond))) {
execBlock(rt, wh.body);
}
return;
}
if (std::holds_alternative<Stmt::Repeat>(s.node)) {
const auto& rp = std::get<Stmt::Repeat>(s.node);
qint64 count = toInt(evalExpr(rt, *rp.count));
if (count < 0) count = 0;
const qint64 startPos = rt.in->device() ? rt.in->device()->pos() : -1;
LogManager::instance().addEntry(QString("[REPEAT] Starting repeat loop: count=%1, pos=0x%2")
.arg(count)
.arg(startPos, 0, 16));
// provide _i for repeat index
const QVariant old = rt.vars.value("_i");
const bool hadOld = rt.vars.contains("_i");
for (qint64 i = 0; i < count; i++) {
const qint64 iterPos = rt.in->device() ? rt.in->device()->pos() : -1;
LogManager::instance().addEntry(QString("[REPEAT] Iteration %1/%2 at pos 0x%3")
.arg(i)
.arg(count)
.arg(iterPos, 0, 16));
rt.vars["_i"] = i;
execBlock(rt, rp.body);
}
const qint64 endPos = rt.in->device() ? rt.in->device()->pos() : -1;
LogManager::instance().addEntry(QString("[REPEAT] Finished repeat loop at pos 0x%1")
.arg(endPos, 0, 16));
if (hadOld) rt.vars["_i"] = old;
else rt.vars.remove("_i");
return;
}
if (std::holds_alternative<Stmt::ForRange>(s.node)) {
const auto& fr = std::get<Stmt::ForRange>(s.node);
qint64 start = toInt(evalExpr(rt, *fr.start));
qint64 end = toInt(evalExpr(rt, *fr.end));
if (end < start) end = start;
const QVariant old = rt.vars.value(fr.var);
const bool hadOld = rt.vars.contains(fr.var);
for (qint64 i = start; i < end; i++) {
rt.vars[fr.var] = i;
execBlock(rt, fr.body);
}
if (hadOld) rt.vars[fr.var] = old;
else rt.vars.remove(fr.var);
return;
}
if (std::holds_alternative<Stmt::Require>(s.node)) {
const auto& rq = std::get<Stmt::Require>(s.node);
const bool ok = toBool(evalExpr(rt, *rq.cond));
if (!ok) throw std::runtime_error("criteria require failed");
return;
}
if (std::holds_alternative<Stmt::SetByteOrder>(s.node)) {
const auto& bo = std::get<Stmt::SetByteOrder>(s.node);
rt.order = bo.order;
applyByteOrder(rt);
return;
}
if (std::holds_alternative<Stmt::CallStmt>(s.node)) {
const auto& cs = std::get<Stmt::CallStmt>(s.node);
evalExpr(rt, *cs.call); // Execute function, discard result
return;
}
throw std::runtime_error("Unknown statement node");
}