#include "jinja-lexer.h" #include "jinja-vm.h" #include "jinja-parser.h" #include "jinja-value.h" #include #include #include #include #define JJ_DEBUG(msg, ...) printf("jinja-vm:%3d : " msg "\n", __LINE__, __VA_ARGS__) //#define JJ_DEBUG(msg, ...) // no-op namespace jinja { template static bool is_stmt(const statement_ptr & ptr) { return dynamic_cast(ptr.get()) != nullptr; } static value_array exec_statements(const statements & stmts, context & ctx) { auto result = mk_val(); for (const auto & stmt : stmts) { JJ_DEBUG("Executing statement of type %s", stmt->type().c_str()); result->val_arr->push_back(stmt->execute(ctx)); } return result; } value identifier::execute(context & ctx) { auto it = ctx.var.find(val); auto builtins = global_builtins(); if (it != ctx.var.end()) { JJ_DEBUG("Identifier '%s' found", val.c_str()); return it->second->clone(); } else if (builtins.find(val) != builtins.end()) { JJ_DEBUG("Identifier '%s' found in builtins", val.c_str()); return mk_val(builtins.at(val), val); } else { JJ_DEBUG("Identifier '%s' not found, returning undefined", val.c_str()); return mk_val(); } } value binary_expression::execute(context & ctx) { value left_val = left->execute(ctx); JJ_DEBUG("Executing binary expression %s '%s' %s", left_val->type().c_str(), op.value.c_str(), right->type().c_str()); // Logical operators if (op.value == "and") { return left_val->as_bool() ? right->execute(ctx) : std::move(left_val); } else if (op.value == "or") { return left_val->as_bool() ? std::move(left_val) : right->execute(ctx); } // Equality operators value right_val = right->execute(ctx); if (op.value == "==") { return mk_val(value_compare(left_val, right_val)); } else if (op.value == "!=") { return mk_val(!value_compare(left_val, right_val)); } // Handle undefined and null values if (is_val(left_val) || is_val(right_val)) { if (is_val(right_val) && (op.value == "in" || op.value == "not in")) { // Special case: `anything in undefined` is `false` and `anything not in undefined` is `true` return mk_val(op.value == "not in"); } throw std::runtime_error("Cannot perform operation " + op.value + " on undefined values"); } else if (is_val(left_val) || is_val(right_val)) { throw std::runtime_error("Cannot perform operation on null values"); } // Float operations if ((is_val(left_val) || is_val(left_val)) && (is_val(right_val) || is_val(right_val))) { double a = left_val->as_float(); double b = right_val->as_float(); if (op.value == "+" || op.value == "-" || op.value == "*") { double res = (op.value == "+") ? a + b : (op.value == "-") ? a - b : a * b; JJ_DEBUG("Arithmetic operation: %f %s %f = %f", a, op.value.c_str(), b, res); bool is_float = is_val(left_val) || is_val(right_val); if (is_float) { return mk_val(res); } else { return mk_val(static_cast(res)); } } else if (op.value == "/") { return mk_val(a / b); } else if (op.value == "%") { double rem = std::fmod(a, b); JJ_DEBUG("Modulo operation: %f %% %f = %f", a, b, rem); bool is_float = is_val(left_val) || is_val(right_val); if (is_float) { return mk_val(rem); } else { return mk_val(static_cast(rem)); } } else if (op.value == "<") { return mk_val(a < b); } else if (op.value == ">") { return mk_val(a > b); } else if (op.value == ">=") { return mk_val(a >= b); } else if (op.value == "<=") { return mk_val(a <= b); } } // Array operations if (is_val(left_val) && is_val(right_val)) { if (op.value == "+") { auto & left_arr = left_val->as_array(); auto & right_arr = right_val->as_array(); auto result = mk_val(); for (const auto & item : left_arr) { result->val_arr->push_back(item->clone()); } for (const auto & item : right_arr) { result->val_arr->push_back(item->clone()); } return result; } } else if (is_val(right_val)) { auto & arr = right_val->as_array(); bool member = std::find_if(arr.begin(), arr.end(), [&](const value& v) { return v == left_val; }) != arr.end(); if (op.value == "in") { return mk_val(member); } else if (op.value == "not in") { return mk_val(!member); } } // String concatenation with ~ and + if ((is_val(left_val) || is_val(right_val)) && (op.value == "~" || op.value == "+")) { JJ_DEBUG("String concatenation with %s operator", op.value.c_str()); auto output = left_val->as_string().append(right_val->as_string()); auto res = mk_val(); res->val_str = std::move(output); return res; } // String membership if (is_val(left_val) && is_val(right_val)) { auto left_str = left_val->as_string().str(); auto right_str = right_val->as_string().str(); if (op.value == "in") { return mk_val(right_str.find(left_str) != std::string::npos); } else if (op.value == "not in") { return mk_val(right_str.find(left_str) == std::string::npos); } } // String in object if (is_val(left_val) && is_val(right_val)) { auto key = left_val->as_string().str(); auto & obj = right_val->as_object(); bool has_key = obj.find(key) != obj.end(); if (op.value == "in") { return mk_val(has_key); } else if (op.value == "not in") { return mk_val(!has_key); } } throw std::runtime_error("Unknown operator \"" + op.value + "\" between " + left_val->type() + " and " + right_val->type()); } static value try_builtin_func(const std::string & name, const value & input, bool undef_on_missing = true) { auto builtins = input->get_builtins(); auto it = builtins.find(name); if (it != builtins.end()) { JJ_DEBUG("Binding built-in '%s'", name.c_str()); return mk_val(it->second, input, name); } if (undef_on_missing) { return mk_val(); } throw std::runtime_error("Unknown (built-in) filter '" + name + "' for type " + input->type()); } value filter_expression::execute(context & ctx) { value input = operand->execute(ctx); if (is_stmt(filter)) { auto filter_val = dynamic_cast(filter.get())->val; if (filter_val == "to_json") { // TODO: Implement to_json filter throw std::runtime_error("to_json filter not implemented"); } if (filter_val == "trim") { filter_val = "strip"; // alias } JJ_DEBUG("Applying filter '%s' to %s", filter_val.c_str(), input->type().c_str()); return try_builtin_func(filter_val, input)->invoke({}); } else if (is_stmt(filter)) { // TODO // value filter_func = filter->execute(ctx); throw std::runtime_error("Filter with arguments not implemented"); } else { throw std::runtime_error("Invalid filter expression"); } } value test_expression::execute(context & ctx) { // NOTE: "value is something" translates to function call "test_is_something(value)" const auto & builtins = global_builtins(); if (!is_stmt(test)) { throw std::runtime_error("Invalid test expression"); } auto test_id = dynamic_cast(test.get())->val; auto it = builtins.find("test_is_" + test_id); JJ_DEBUG("Test expression %s '%s'", operand->type().c_str(), test_id.c_str()); if (it == builtins.end()) { throw std::runtime_error("Unknown test '" + test_id + "'"); } func_args args; args.args.push_back(operand->execute(ctx)); return it->second(args); } value unary_expression::execute(context & ctx) { value operand_val = argument->execute(ctx); JJ_DEBUG("Executing unary expression with operator '%s'", op.value.c_str()); if (op.value == "not") { return mk_val(!operand_val->as_bool()); } else if (op.value == "-") { if (is_val(operand_val)) { return mk_val(-operand_val->as_int()); } else if (is_val(operand_val)) { return mk_val(-operand_val->as_float()); } else { throw std::runtime_error("Unary - operator requires numeric operand"); } } throw std::runtime_error("Unknown unary operator '" + op.value + "'"); } value if_statement::execute(context & ctx) { value test_val = test->execute(ctx); auto out = mk_val(); if (test_val->as_bool()) { for (auto & stmt : body) { JJ_DEBUG("IF --> Executing THEN body, current block: %s", stmt->type().c_str()); out->val_arr->push_back(stmt->execute(ctx)); } } else { for (auto & stmt : alternate) { JJ_DEBUG("IF --> Executing ELSE body, current block: %s", stmt->type().c_str()); out->val_arr->push_back(stmt->execute(ctx)); } } return out; } value for_statement::execute(context & ctx) { context scope(ctx); // new scope for loop variables statement_ptr iter_expr = std::move(iterable); statement_ptr test_expr = nullptr; if (is_stmt(iterable)) { JJ_DEBUG("%s", "For loop has test expression"); auto select = dynamic_cast(iterable.get()); iter_expr = std::move(select->lhs); test_expr = std::move(select->test); } JJ_DEBUG("Executing for statement, iterable type: %s", iter_expr->type().c_str()); value iterable_val = iter_expr->execute(scope); if (!is_val(iterable_val) && !is_val(iterable_val)) { throw std::runtime_error("Expected iterable or object type in for loop: got " + iterable_val->type()); } std::vector items; if (is_val(iterable_val)) { auto & obj = iterable_val->as_object(); for (auto & p : obj) { items.push_back(mk_val(p.first)); } } else { auto & arr = iterable_val->as_array(); for (const auto & item : arr) { items.push_back(item->clone()); } } std::vector> scope_update_fns; std::vector filtered_items; for (size_t i = 0; i < items.size(); ++i) { context loop_scope(scope); const value & current = items[i]; std::function scope_update_fn = [](context &) { /* no-op */}; if (is_stmt(loopvar)) { auto id = dynamic_cast(loopvar.get())->val; scope_update_fn = [id, &items, i](context & ctx) { ctx.var[id] = items[i]->clone(); }; } else if (is_stmt(loopvar)) { auto tuple = dynamic_cast(loopvar.get()); if (!is_val(current)) { throw std::runtime_error("Cannot unpack non-iterable type: " + current->type()); } auto & c_arr = current->as_array(); if (tuple->val.size() != c_arr.size()) { throw std::runtime_error(std::string("Too ") + (tuple->val.size() > c_arr.size() ? "few" : "many") + " items to unpack"); } scope_update_fn = [tuple, &items, i](context & ctx) { auto & c_arr = items[i]->as_array(); for (size_t j = 0; j < tuple->val.size(); ++j) { if (!is_stmt(tuple->val[j])) { throw std::runtime_error("Cannot unpack non-identifier type: " + tuple->val[j]->type()); } auto id = dynamic_cast(tuple->val[j].get())->val; ctx.var[id] = c_arr[j]->clone(); } }; } else { throw std::runtime_error("Invalid loop variable(s): " + loopvar->type()); } if (test_expr) { scope_update_fn(loop_scope); value test_val = test_expr->execute(loop_scope); if (!test_val->as_bool()) { continue; } } filtered_items.push_back(current->clone()); scope_update_fns.push_back(scope_update_fn); } auto result = mk_val(); bool noIteration = true; for (size_t i = 0; i < filtered_items.size(); ++i) { JJ_DEBUG("For loop iteration %zu/%zu", i + 1, filtered_items.size()); value_object loop_obj = mk_val(); loop_obj->insert("index", mk_val(i + 1)); loop_obj->insert("index0", mk_val(i)); loop_obj->insert("revindex", mk_val(filtered_items.size() - i)); loop_obj->insert("revindex0", mk_val(filtered_items.size() - i - 1)); loop_obj->insert("first", mk_val(i == 0)); loop_obj->insert("last", mk_val(i == filtered_items.size() - 1)); loop_obj->insert("length", mk_val(filtered_items.size())); loop_obj->insert("previtem", i > 0 ? filtered_items[i - 1]->clone() : mk_val()); loop_obj->insert("nextitem", i < filtered_items.size() - 1 ? filtered_items[i + 1]->clone() : mk_val()); ctx.var["loop"] = loop_obj->clone(); scope_update_fns[i](ctx); try { for (auto & stmt : body) { value val = stmt->execute(ctx); result->push_back(val); } } catch (const continue_statement::exception &) { continue; } catch (const break_statement::exception &) { break; } noIteration = false; } if (noIteration) { for (auto & stmt : default_block) { value val = stmt->execute(ctx); result->push_back(val); } } return result; } value set_statement::execute(context & ctx) { auto rhs = val ? val->execute(ctx) : exec_statements(body, ctx); if (is_stmt(assignee)) { auto var_name = dynamic_cast(assignee.get())->val; JJ_DEBUG("Setting variable '%s' with value type %s", var_name.c_str(), rhs->type().c_str()); ctx.var[var_name] = rhs->clone(); } else if (is_stmt(assignee)) { auto tuple = dynamic_cast(assignee.get()); if (!is_val(rhs)) { throw std::runtime_error("Cannot unpack non-iterable type in set: " + rhs->type()); } auto & arr = rhs->as_array(); if (arr.size() != tuple->val.size()) { throw std::runtime_error(std::string("Too ") + (tuple->val.size() > arr.size() ? "few" : "many") + " items to unpack in set"); } for (size_t i = 0; i < tuple->val.size(); ++i) { auto & elem = tuple->val[i]; if (!is_stmt(elem)) { throw std::runtime_error("Cannot unpack to non-identifier in set: " + elem->type()); } auto var_name = dynamic_cast(elem.get())->val; ctx.var[var_name] = arr[i]->clone(); } } else if (is_stmt(assignee)) { auto member = dynamic_cast(assignee.get()); if (member->computed) { throw std::runtime_error("Cannot assign to computed member"); } if (!is_stmt(member->property)) { throw std::runtime_error("Cannot assign to member with non-identifier property"); } auto prop_name = dynamic_cast(member->property.get())->val; value object = member->object->execute(ctx); if (!is_val(object)) { throw std::runtime_error("Cannot assign to member of non-object"); } auto obj_ptr = dynamic_cast(object.get()); JJ_DEBUG("Setting object property '%s'", prop_name.c_str()); obj_ptr->insert(prop_name, rhs->clone()); } else { throw std::runtime_error("Invalid LHS inside assignment expression: " + assignee->type()); } return mk_val(); } value macro_statement::execute(context & ctx) { std::string name = dynamic_cast(this->name.get())->val; const func_handler func = [this, &ctx, name](const func_args & args) -> value { JJ_DEBUG("Invoking macro '%s' with %zu arguments", name.c_str(), args.args.size()); context macro_ctx(ctx); // new scope for macro execution // bind parameters size_t param_count = this->args.size(); size_t arg_count = args.args.size(); for (size_t i = 0; i < param_count; ++i) { std::string param_name = dynamic_cast(this->args[i].get())->val; if (i < arg_count) { macro_ctx.var[param_name] = args.args[i]->clone(); } else { macro_ctx.var[param_name] = mk_val(); } } // execute macro body return exec_statements(this->body, macro_ctx); }; JJ_DEBUG("Defining macro '%s' with %zu parameters", name.c_str(), args.size()); ctx.var[name] = mk_val(func); return mk_val(); } value member_expression::execute(context & ctx) { value object = this->object->execute(ctx); value property; if (this->computed) { JJ_DEBUG("Member expression, computing property type %s", this->property->type().c_str()); if (is_stmt(this->property)) { auto s = dynamic_cast(this->property.get()); value start_val = s->start_expr ? s->start_expr->execute(ctx) : mk_val(); value stop_val = s->stop_expr ? s->stop_expr->execute(ctx) : mk_val(); value step_val = s->step_expr ? s->step_expr->execute(ctx) : mk_val(); // translate to function call: obj.slice(start, stop, step) JJ_DEBUG("Member expression is a slice: start %s, stop %s, step %s", start_val->as_repr().c_str(), stop_val->as_repr().c_str(), step_val->as_repr().c_str()); auto slice_func = try_builtin_func("slice", object); func_args args; args.args.push_back(start_val->clone()); args.args.push_back(stop_val->clone()); args.args.push_back(step_val->clone()); return slice_func->invoke(args); } else { property = this->property->execute(ctx); } } else { property = mk_val(dynamic_cast(this->property.get())->val); } JJ_DEBUG("Member expression on object type %s, property type %s", object->type().c_str(), property->type().c_str()); value val = mk_val(); if (is_val(object)) { if (!is_val(property)) { throw std::runtime_error("Cannot access object with non-string: got " + property->type()); } auto key = property->as_string().str(); JJ_DEBUG("Accessing object property '%s'", key.c_str()); auto & obj = object->as_object(); auto it = obj.find(key); if (it != obj.end()) { val = it->second->clone(); } else { val = try_builtin_func(key, object, true); } } else if (is_val(object) || is_val(object)) { if (is_val(property)) { int64_t index = property->as_int(); JJ_DEBUG("Accessing %s index %lld", is_val(object) ? "array" : "string", index); if (is_val(object)) { auto & arr = object->as_array(); if (index >= 0 && index < static_cast(arr.size())) { val = arr[index]->clone(); } } else { // value_string auto str = object->as_string().str(); if (index >= 0 && index < static_cast(str.size())) { val = mk_val(std::string(1, str[index])); } } } else if (is_val(property)) { auto key = property->as_string().str(); JJ_DEBUG("Accessing %s built-in '%s'", is_val(object) ? "array" : "string", key.c_str()); val = try_builtin_func(key, object); } else { throw std::runtime_error("Cannot access property with non-string/non-number: got " + property->type()); } } else { if (!is_val(property)) { throw std::runtime_error("Cannot access property with non-string: got " + property->type()); } auto key = property->as_string().str(); val = try_builtin_func(key, object); } return val; } value call_expression::execute(context & ctx) { // gather arguments func_args args; for (auto & arg_stmt : this->args) { auto arg_val = arg_stmt->execute(ctx); JJ_DEBUG(" Argument type: %s", arg_val->type().c_str()); args.args.push_back(std::move(arg_val)); } // execute callee value callee_val = callee->execute(ctx); if (!is_val(callee_val)) { throw std::runtime_error("Callee is not a function: got " + callee_val->type()); } auto * callee_func = dynamic_cast(callee_val.get()); JJ_DEBUG("Calling function '%s' with %zu arguments", callee_func->name.c_str(), args.args.size()); return callee_func->invoke(args); } // compare operator for value_t bool value_compare(const value & a, const value & b) { JJ_DEBUG("Comparing types: %s and %s", a->type().c_str(), b->type().c_str()); // compare numeric types if ((is_val(a) || is_val(a)) && (is_val(b) || is_val(b))){ try { return a->as_float() == b->as_float(); } catch (...) {} } // compare string and number // TODO: not sure if this is the right behavior if ((is_val(b) && (is_val(a) || is_val(a))) || (is_val(a) && (is_val(b) || is_val(b)))) { try { return a->as_string().str() == b->as_string().str(); } catch (...) {} } // compare boolean simple if (is_val(a) && is_val(b)) { return a->as_bool() == b->as_bool(); } // compare string simple if (is_val(a) && is_val(b)) { return a->as_string().str() == b->as_string().str(); } // compare by type if (a->type() != b->type()) { return false; } return false; } value keyword_argument_expression::execute(context & ctx) { if (!is_stmt(key)) { throw std::runtime_error("Keyword argument key must be identifiers"); } std::string k = dynamic_cast(key.get())->val; JJ_DEBUG("Keyword argument expression key: %s, value: %s", k.c_str(), val->type().c_str()); value v = val->execute(ctx); JJ_DEBUG("Keyword argument value executed, type: %s", v->type().c_str()); return mk_val(k, v); } } // namespace jinja