#include "jinja-lexer.h" #include "jinja-vm.h" #include "jinja-parser.h" #include "jinja-value.h" #include "jinja-utils.h" #include #include #include #include #define FILENAME "jinja-vm" bool g_jinja_debug = false; namespace jinja { void enable_debug(bool enable) { g_jinja_debug = enable; } static value_string 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->push_back(stmt->execute(ctx)); } // convert to string parts value_string str = mk_val(); gather_string_parts_recursive(result, str); return str; } // execute with error handling value statement::execute(context & ctx) { try { return execute_impl(ctx); } catch (const continue_statement::signal & ex) { throw ex; } catch (const break_statement::signal & ex) { throw ex; } catch (const std::exception & e) { if (ctx.source.empty()) { std::ostringstream oss; oss << "\nError executing " << type() << " at position " << pos << ": " << e.what(); throw raised_exception(oss.str()); } else { std::ostringstream oss; constexpr int max_peak_chars = 40; oss << "\n------------\n"; oss << "While executing " << type() << " at position " << pos << " in source:\n"; size_t start = (pos >= max_peak_chars) ? (pos - max_peak_chars) : 0; size_t end = std::min(pos + max_peak_chars, ctx.source.length()); std::string substr = ctx.source.substr(start, end - start); string_replace_all(substr, "\n", "\\n"); oss << "..." << substr << "...\n"; std::string spaces(pos - start + 3, ' '); oss << spaces << "^\n"; oss << "Error: " << e.what(); throw raised_exception(oss.str()); } } } value identifier::execute_impl(context & ctx) { auto it = ctx.get_val(val); auto builtins = global_builtins(); if (!it->is_undefined()) { JJ_DEBUG("Identifier '%s' found", val.c_str()); return it; } else if (builtins.find(val) != builtins.end()) { JJ_DEBUG("Identifier '%s' found in builtins", val.c_str()); return mk_val(val, builtins.at(val)); } else { JJ_DEBUG("Identifier '%s' not found, returning undefined", val.c_str()); return mk_val(val); } } value object_literal::execute_impl(context & ctx) { auto obj = mk_val(); for (const auto & pair : val) { std::string key = pair.first->execute(ctx)->as_string().str(); value val = pair.second->execute(ctx); JJ_DEBUG("Object literal: setting key '%s' of type %s", key.c_str(), val->type().c_str()); obj->val_obj[key] = val; } return obj; } value binary_expression::execute_impl(context & ctx) { value left_val = left->execute(ctx); // 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); JJ_DEBUG("Executing binary expression %s '%s' %s", left_val->type().c_str(), op.value.c_str(), right_val->type().c_str()); if (op.value == "==") { ctx.mark_known_type(left_val, right_val); ctx.mark_known_type(right_val, left_val); 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"); } // if (ctx.wrk_around.string_plus_undefined_is_string && (op.value == "+" || op.value == "~")) { // JJ_DEBUG("%s", "Workaround: treating undefined as empty string for string concatenation"); // auto left_str = left_val->is_undefined() ? string() : left_val->as_string(); // auto right_str = right_val->is_undefined() ? string() : right_val->as_string(); // auto output = left_str.append(right_str); // auto res = mk_val(); // res->val_str = std::move(output); // return res; // } 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 == "/") { JJ_DEBUG("Division operation: %f / %f", a, b); 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 == "<") { JJ_DEBUG("Comparison operation: %f < %f is %d", a, b, a < b); return mk_val(a < b); } else if (op.value == ">") { JJ_DEBUG("Comparison operation: %f > %f is %d", a, b, a > b); return mk_val(a > b); } else if (op.value == ">=") { JJ_DEBUG("Comparison operation: %f >= %f is %d", a, b, a >= b); return mk_val(a >= b); } else if (op.value == "<=") { JJ_DEBUG("Comparison operation: %f <= %f is %d", a, b, a <= b); 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->push_back(item); } for (const auto & item : right_arr) { result->push_back(item); } return result; } } else if (is_val(right_val)) { auto & arr = right_val->as_array(); bool member = false; for (const auto & item : arr) { if (value_compare(left_val, item)) { member = true; break; } } if (op.value == "in") { JJ_DEBUG("Checking membership: %s in Array is %d", left_val->type().c_str(), member); return mk_val(member); } else if (op.value == "not in") { JJ_DEBUG("Checking non-membership: %s not in Array is %d", left_val->type().c_str(), !member); 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 = false) { 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(name, it->second, input); } if (undef_on_missing) { return mk_val(name); } throw std::runtime_error("Unknown (built-in) filter '" + name + "' for type " + input->type()); } value filter_expression::execute_impl(context & ctx) { value input = operand ? operand->execute(ctx) : val; JJ_DEBUG("Applying filter to %s", input->type().c_str()); if (is_stmt(filter)) { auto filter_id = cast_stmt(filter)->val; if (filter_id == "to_json") { // TODO: Implement to_json filter throw std::runtime_error("to_json filter not implemented"); } if (filter_id == "trim") { filter_id = "strip"; // alias } JJ_DEBUG("Applying filter '%s' to %s", filter_id.c_str(), input->type().c_str()); return try_builtin_func(filter_id, input)->invoke(func_args(ctx)); } else if (is_stmt(filter)) { auto call = cast_stmt(filter); auto filter_id = cast_stmt(call->callee)->val; JJ_DEBUG("Applying filter '%s' with arguments to %s", filter_id.c_str(), input->type().c_str()); func_args args(ctx); for (const auto & arg_expr : call->args) { args.args.push_back(arg_expr->execute(ctx)); } return try_builtin_func(filter_id, input)->invoke(args); } else { throw std::runtime_error("Invalid filter expression"); } } value filter_statement::execute_impl(context & ctx) { // eval body as string, then apply filter auto body_val = exec_statements(body, ctx); value_string parts = mk_val(); gather_string_parts_recursive(body_val, parts); JJ_DEBUG("FilterStatement: applying filter to body string of length %zu", parts->val_str.length()); filter_expression filter_expr(std::move(parts), std::move(filter)); return filter_expr.execute(ctx); } value test_expression::execute_impl(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 = cast_stmt(test)->val; auto it = builtins.find("test_is_" + test_id); JJ_DEBUG("Test expression %s '%s' %s (using function 'test_is_%s')", operand->type().c_str(), test_id.c_str(), negate ? "(negate)" : "", test_id.c_str()); if (it == builtins.end()) { throw std::runtime_error("Unknown test '" + test_id + "'"); } value input = operand->execute(ctx); func_args args(ctx); args.args.push_back(input); auto res = it->second(args); // hack: allow type inference if (test_id == "defined" || test_id == "undefined" || test_id == "none") { ctx.mark_known_type(input, inferred_type::optional); } else if (test_id == "string") { ctx.mark_known_type(input, inferred_type::string); } if (negate) { return mk_val(!res->as_bool()); } else { return res; } } value unary_expression::execute_impl(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_impl(context & ctx) { value test_val = test->execute(ctx); ctx.mark_known_type(test_val, inferred_type::boolean); ctx.mark_known_type(test_val, inferred_type::optional); 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->push_back(stmt->execute(ctx)); } } else { for (auto & stmt : alternate) { JJ_DEBUG("IF --> Executing ELSE body, current block: %s", stmt->type().c_str()); out->push_back(stmt->execute(ctx)); } } // convert to string parts value_string str = mk_val(); gather_string_parts_recursive(out, str); return str; } value for_statement::execute_impl(context & ctx) { context scope(ctx); // new scope for loop variables jinja::select_expression * select_expr = cast_stmt(iterable); statement_ptr test_expr_nullptr; statement_ptr & iter_expr = [&]() -> statement_ptr & { auto tmp = cast_stmt(iterable); return tmp ? tmp->lhs : iterable; }(); statement_ptr & test_expr = [&]() -> statement_ptr & { auto tmp = cast_stmt(iterable); return tmp ? tmp->test : test_expr_nullptr; }(); JJ_DEBUG("Executing for statement, iterable type: %s", iter_expr->type().c_str()); value iterable_val = iter_expr->execute(scope); if (iterable_val->is_undefined()) { JJ_DEBUG("%s", "For loop iterable is undefined, skipping loop"); iterable_val = mk_val(); } ctx.mark_known_type(iterable_val, inferred_type::array); ctx.mark_known_type(iterable_val, inferred_type::object); 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)) { JJ_DEBUG("%s", "For loop over object keys"); auto & obj = iterable_val->as_object(); for (auto & p : obj) { auto tuple = mk_val(); tuple->push_back(mk_val(p.first)); tuple->push_back(p.second); items.push_back(tuple); } } else { JJ_DEBUG("%s", "For loop over array items"); auto & arr = iterable_val->as_array(); for (const auto & item : arr) { items.push_back(item); } } 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 = cast_stmt(loopvar)->val; scope_update_fn = [id, &items, i](context & ctx) { ctx.set_val(id, items[i]); }; } else if (is_stmt(loopvar)) { auto tuple = cast_stmt(loopvar); 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 = cast_stmt(tuple->val[j])->val; ctx.set_val(id, c_arr[j]); } }; } else { throw std::runtime_error("Invalid loop variable(s): " + loopvar->type()); } if (select_expr && test_expr) { scope_update_fn(loop_scope); value test_val = test_expr->execute(loop_scope); if (!test_val->as_bool()) { continue; } } JJ_DEBUG("For loop: adding item type %s at index %zu", current->type().c_str(), i); filtered_items.push_back(current); scope_update_fns.push_back(scope_update_fn); } JJ_DEBUG("For loop: %zu items after filtering", filtered_items.size()); 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] : mk_val("previtem")); loop_obj->insert("nextitem", i < filtered_items.size() - 1 ? filtered_items[i + 1] : mk_val("nextitem")); scope.set_val("loop", loop_obj); scope_update_fns[i](scope); try { for (auto & stmt : body) { value val = stmt->execute(scope); result->push_back(val); } } catch (const continue_statement::signal &) { continue; } catch (const break_statement::signal &) { break; } noIteration = false; } JJ_DEBUG("For loop complete, total iterations: %zu", filtered_items.size()); if (noIteration) { for (auto & stmt : default_block) { value val = stmt->execute(ctx); result->push_back(val); } } // convert to string parts value_string str = mk_val(); gather_string_parts_recursive(result, str); return str; } value set_statement::execute_impl(context & ctx) { auto rhs = val ? val->execute(ctx) : exec_statements(body, ctx); if (is_stmt(assignee)) { auto var_name = cast_stmt(assignee)->val; JJ_DEBUG("Setting variable '%s' with value type %s", var_name.c_str(), rhs->type().c_str()); ctx.set_val(var_name, rhs); } else if (is_stmt(assignee)) { auto tuple = cast_stmt(assignee); 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 = cast_stmt(elem)->val; ctx.set_val(var_name, arr[i]); } } else if (is_stmt(assignee)) { auto member = cast_stmt(assignee); 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 = cast_stmt(member->property)->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 = cast_val(object); JJ_DEBUG("Setting object property '%s'", prop_name.c_str()); obj_ptr->insert(prop_name, rhs); } else { throw std::runtime_error("Invalid LHS inside assignment expression: " + assignee->type()); } return mk_val(); } value macro_statement::execute_impl(context & ctx) { std::string name = cast_stmt(this->name)->val; const func_handler func = [this, name, &ctx](const func_args & args) -> value { size_t expected_count = this->args.size(); size_t input_count = args.args.size(); JJ_DEBUG("Invoking macro '%s' with %zu input arguments (expected %zu)", name.c_str(), input_count, expected_count); context macro_ctx(ctx); // new scope for macro execution // bind parameters for (size_t i = 0; i < expected_count; ++i) { if (i < input_count) { std::string param_name = cast_stmt(this->args[i])->val; JJ_DEBUG(" Binding parameter '%s' to argument of type %s", param_name.c_str(), args.args[i]->type().c_str()); macro_ctx.set_val(param_name, args.args[i]); } else { auto & default_arg = this->args[i]; if (is_stmt(default_arg)) { auto kwarg = cast_stmt(default_arg); std::string param_name = cast_stmt(kwarg->key)->val; JJ_DEBUG(" Binding parameter '%s' to default argument of type %s", param_name.c_str(), kwarg->val->type().c_str()); macro_ctx.set_val(param_name, kwarg->val->execute(ctx)); } else { throw std::runtime_error("Not enough arguments provided to macro '" + name + "'"); } //std::string param_name = cast_stmt(default_args[i])->val; //JJ_DEBUG(" Binding parameter '%s' to default", param_name.c_str()); //macro_ctx.var[param_name] = default_args[i]->execute(ctx); } } // execute macro body JJ_DEBUG("Executing macro '%s' body with %zu statements", name.c_str(), this->body.size()); auto res = exec_statements(this->body, macro_ctx); JJ_DEBUG("Macro '%s' execution complete, result: %s", name.c_str(), res->val_str.str().c_str()); return res; }; JJ_DEBUG("Defining macro '%s' with %zu parameters", name.c_str(), args.size()); ctx.set_val(name, mk_val(name, func)); return mk_val(); } value member_expression::execute_impl(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 = cast_stmt(this->property); value start_val = s->start_expr ? s->start_expr->execute(ctx) : mk_val("start"); value stop_val = s->stop_expr ? s->stop_expr->execute(ctx) : mk_val("stop"); value step_val = s->step_expr ? s->step_expr->execute(ctx) : mk_val("step"); // 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(ctx); args.args.push_back(start_val); args.args.push_back(stop_val); args.args.push_back(step_val); return slice_func->invoke(args); } else { property = this->property->execute(ctx); } } else { property = mk_val(cast_stmt(this->property)->val); } JJ_DEBUG("Member expression on object type %s, property type %s", object->type().c_str(), property->type().c_str()); value val = mk_val("object_property"); if (is_val(object)) { JJ_DEBUG("%s", "Accessing property on undefined object, returning undefined"); return val; } else 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(); auto & obj = object->as_object(); auto it = obj.find(key); if (it != obj.end()) { val = it->second; } else { val = try_builtin_func(key, object, true); } JJ_DEBUG("Accessed property '%s' value, got type: %s", key.c_str(), val->type().c_str()); } else if (is_val(object) || is_val(object)) { if (is_val(property)) { int64_t index = property->as_int(); JJ_DEBUG("Accessing %s index %lld", object->type().c_str(), index); if (is_val(object)) { auto & arr = object->as_array(); if (index < 0) { index += static_cast(arr.size()); } if (index >= 0 && index < static_cast(arr.size())) { val = arr[index]; } } 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_impl(context & ctx) { // gather arguments func_args args(ctx); 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 = cast_val(callee_val); 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) { auto cmp = [&]() { // 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; }; auto result = cmp(); JJ_DEBUG("Comparing types: %s and %s result=%d", a->type().c_str(), b->type().c_str(), result); return result; } value keyword_argument_expression::execute_impl(context & ctx) { if (!is_stmt(key)) { throw std::runtime_error("Keyword argument key must be identifiers"); } std::string k = cast_stmt(key)->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