diff --git a/common/jinja/jinja-parser.cpp b/common/jinja/jinja-parser.cpp index de61023560..8b7058b8fa 100644 --- a/common/jinja/jinja-parser.cpp +++ b/common/jinja/jinja-parser.cpp @@ -142,11 +142,11 @@ private: } else if (name == "call") { statements caller_args; - bool has_caller_args = false; + // bool has_caller_args = false; if (is(token::open_paren)) { // Optional caller arguments, e.g. {% call(user) dump_users(...) %} caller_args = parse_args(); - has_caller_args = true; + // has_caller_args = true; } auto callee = parse_primary_expression(); if (!is_type(callee)) throw std::runtime_error("Expected identifier"); diff --git a/common/jinja/jinja-value.h b/common/jinja/jinja-value.h index a5362169c4..8b2d74ae35 100644 --- a/common/jinja/jinja-value.h +++ b/common/jinja/jinja-value.h @@ -28,8 +28,13 @@ bool is_val(const value & ptr) { using PointeeType = typename extract_pointee::type; return dynamic_cast(ptr.get()) != nullptr; } +template +bool is_val(const value_t * ptr) { + using PointeeType = typename extract_pointee::type; + return dynamic_cast(ptr) != nullptr; +} template -value mk_val(Args&&... args) { +std::unique_ptr::type> mk_val(Args&&... args) { using PointeeType = typename extract_pointee::type; return std::make_unique(std::forward(args)...); } @@ -70,6 +75,8 @@ struct func_args { using func_handler = std::function; using func_builtins = std::map; +bool value_compare(const value & a, const value & b); + struct value_t { int64_t val_int; double val_flt; @@ -93,12 +100,12 @@ struct value_t { virtual std::string type() const { return ""; } - virtual int64_t as_int() const { throw std::runtime_error("Not an int value"); } - virtual double as_float() const { throw std::runtime_error("Not a float value"); } - virtual std::string as_string() const { throw std::runtime_error("Not a string value"); } - virtual bool as_bool() const { throw std::runtime_error("Not a bool value"); } - virtual const std::vector & as_array() const { throw std::runtime_error("Not an array value"); } - virtual const std::map & as_object() const { throw std::runtime_error("Not an object value"); } + virtual int64_t as_int() const { throw std::runtime_error(type() + " is not an int value"); } + virtual double as_float() const { throw std::runtime_error(type() + " is not a float value"); } + virtual std::string as_string() const { throw std::runtime_error(type() + " is not a string value"); } + virtual bool as_bool() const { throw std::runtime_error(type() + " is not a bool value"); } + virtual const std::vector & as_array() const { throw std::runtime_error(type() + " is not an array value"); } + virtual const std::map & as_object() const { throw std::runtime_error(type() + " is not an object value"); } virtual value invoke(const func_args &) const { throw std::runtime_error("Not a function value"); } virtual bool is_null() const { return false; } virtual bool is_undefined() const { return false; } @@ -106,17 +113,11 @@ struct value_t { throw std::runtime_error("No builtins available for type " + type()); } + virtual std::string as_repr() const { return as_string(); } + virtual value clone() const { return std::make_unique(*this); } - - virtual bool operator==(const value & other) const { - // TODO - return false; - } - virtual bool operator!=(const value & other) const { - return !(*this == other); - } }; @@ -188,8 +189,12 @@ struct value_array_t : public value_t { val_arr->push_back(other.val_arr->at(i)->clone()); } } + void push_back(const value & val) { + val_arr->push_back(val->clone()); + } virtual std::string type() const override { return "Array"; } virtual const std::vector & as_array() const override { return *val_arr; } + // clone will also share the underlying data (point to the same vector) virtual value clone() const override { auto tmp = std::make_unique(); tmp->val_arr = this->val_arr; @@ -200,7 +205,7 @@ struct value_array_t : public value_t { ss << "["; for (size_t i = 0; i < val_arr->size(); i++) { if (i > 0) ss << ", "; - ss << val_arr->at(i)->as_string(); + ss << val_arr->at(i)->as_repr(); } ss << "]"; return ss.str(); @@ -224,8 +229,12 @@ struct value_object_t : public value_t { (*val_obj)[pair.first] = pair.second->clone(); } } + void insert(const std::string & key, const value & val) { + (*val_obj)[key] = val->clone(); + } virtual std::string type() const override { return "Object"; } virtual const std::map & as_object() const override { return *val_obj; } + // clone will also share the underlying data (point to the same map) virtual value clone() const override { auto tmp = std::make_unique(); tmp->val_obj = this->val_obj; @@ -244,6 +253,7 @@ struct value_func_t : public value_t { return val_func(args); } virtual std::string type() const override { return "Function"; } + virtual std::string as_repr() const override { return type(); } virtual value clone() const override { return std::make_unique(*this); } }; using value_func = std::unique_ptr; @@ -252,6 +262,8 @@ using value_func = std::unique_ptr; struct value_null_t : public value_t { virtual std::string type() const override { return "Null"; } virtual bool is_null() const override { return true; } + virtual bool as_bool() const override { return false; } + virtual std::string as_repr() const override { return type(); } virtual value clone() const override { return std::make_unique(*this); } }; using value_null = std::unique_ptr; @@ -260,8 +272,13 @@ using value_null = std::unique_ptr; struct value_undefined_t : public value_t { virtual std::string type() const override { return "Undefined"; } virtual bool is_undefined() const override { return true; } + virtual bool as_bool() const override { return false; } + virtual std::string as_repr() const override { return type(); } virtual value clone() const override { return std::make_unique(*this); } }; using value_undefined = std::unique_ptr; + +const func_builtins & global_builtins(); + } // namespace jinja diff --git a/common/jinja/jinja-vm-builtins.cpp b/common/jinja/jinja-vm-builtins.cpp index 860f67b629..493c71e25e 100644 --- a/common/jinja/jinja-vm-builtins.cpp +++ b/common/jinja/jinja-vm-builtins.cpp @@ -8,6 +8,18 @@ namespace jinja { +const func_builtins & global_builtins() { + static const func_builtins builtins = { + {"raise_exception", [](const func_args & args) -> value { + args.ensure_count(1); + std::string msg = args.args[0]->as_string(); + throw raised_exception("Jinja Exception: " + msg); + }}, + }; + return builtins; +} + + const func_builtins & value_int_t::get_builtins() const { static const func_builtins builtins = { {"abs", [](const func_args & args) -> value { @@ -189,10 +201,10 @@ const func_builtins & value_string_t::get_builtins() const { args.ensure_vals(); return mk_val(args.args[0]->as_string()); }}, - {"indent", [](const func_args & args) -> value { + {"indent", [](const func_args &) -> value { throw std::runtime_error("indent builtin not implemented"); }}, - {"join", [](const func_args & args) -> value { + {"join", [](const func_args &) -> value { throw std::runtime_error("join builtin not implemented"); }}, }; @@ -307,5 +319,4 @@ const func_builtins & value_object_t::get_builtins() const { return builtins; } - } // namespace jinja diff --git a/common/jinja/jinja-vm.cpp b/common/jinja/jinja-vm.cpp index 73ad5bae0d..7fb323c58b 100644 --- a/common/jinja/jinja-vm.cpp +++ b/common/jinja/jinja-vm.cpp @@ -18,11 +18,24 @@ 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)); } else { JJ_DEBUG("Identifier '%s' not found, returning undefined", val.c_str()); return mk_val(); @@ -31,6 +44,7 @@ value identifier::execute(context & ctx) { value binary_expression::execute(context & ctx) { value left_val = left->execute(ctx); + JJ_DEBUG("Executing binary expression with operator '%s'", op.value.c_str()); // Logical operators if (op.value == "and") { @@ -42,9 +56,9 @@ value binary_expression::execute(context & ctx) { // Equality operators value right_val = right->execute(ctx); if (op.value == "==") { - return mk_val(left_val == right_val); + return mk_val(value_compare(left_val, right_val)); } else if (op.value == "!=") { - return mk_val(left_val != right_val); + return mk_val(!value_compare(left_val, right_val)); } // Handle undefined and null values @@ -70,6 +84,7 @@ value binary_expression::execute(context & ctx) { 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); @@ -80,6 +95,7 @@ value binary_expression::execute(context & ctx) { 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); @@ -123,6 +139,7 @@ value binary_expression::execute(context & ctx) { // String concatenation if (is_val(left_val) || is_val(right_val)) { + JJ_DEBUG("%s", "String concatenation with + operator"); if (op.value == "+") { return mk_val(left_val->as_string() + right_val->as_string()); } @@ -177,7 +194,6 @@ value filter_expression::execute(context & ctx) { } if (is_val(input)) { - auto & arr = input->as_array(); auto res = try_builtin(filter_val); if (res) { return res; @@ -222,7 +238,12 @@ value if_statement::execute(context & ctx) { auto out = mk_val(); if (test_val->as_bool()) { for (auto & stmt : body) { - JJ_DEBUG("Executing if body statement of type %s", stmt->type().c_str()); + 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)); } } @@ -230,19 +251,171 @@ value if_statement::execute(context & ctx) { } value for_statement::execute(context & ctx) { - throw std::runtime_error("for_statement::execute not implemented"); -} + context scope(ctx); // new scope for loop variables -value break_statement::execute(context & ctx) { - throw std::runtime_error("break_statement::execute not implemented"); -} + statement_ptr iter_expr = std::move(iterable); + statement_ptr test_expr = nullptr; -value continue_statement::execute(context & ctx) { - throw std::runtime_error("continue_statement::execute not implemented"); + 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) { - throw std::runtime_error("set_statement::execute not implemented"); + 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'", var_name.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()); + value object = member->object->execute(ctx); + if (!is_val(object)) { + throw std::runtime_error("Cannot assign to member of non-object"); + } + 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; + auto obj_ptr = dynamic_cast(object.get()); + JJ_DEBUG("Setting object property '%s'", prop_name.c_str()); + obj_ptr->get()->insert(prop_name, rhs->clone()); + + } else { + throw std::runtime_error("Invalid LHS inside assignment expression: " + assignee->type()); + } + return mk_val(); } value member_expression::execute(context & ctx) { @@ -279,6 +452,7 @@ value member_expression::execute(context & ctx) { } 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())) { @@ -292,6 +466,7 @@ value member_expression::execute(context & ctx) { } } else if (is_val(property)) { auto key = property->as_string(); + JJ_DEBUG("Accessing %s built-in '%s'", is_val(object) ? "array" : "string", key.c_str()); auto builtins = object->get_builtins(); auto bit = builtins.find(key); if (bit != builtins.end()) { @@ -320,4 +495,55 @@ value member_expression::execute(context & ctx) { return val; } +static func_args gather_call_args(const statements & arg_stmts, context & ctx) { + func_args args; + for (auto & arg_stmt : arg_stmts) { + args.args.push_back(arg_stmt->execute(ctx)); + } + return args; +} + +value call_expression::execute(context & ctx) { + auto args = gather_call_args(this->args, ctx); + value callee_val = callee->execute(ctx); + JJ_DEBUG("Calling function of type %s with %zu arguments", callee_val->type().c_str(), args.args.size()); + if (!is_val(callee_val)) { + throw std::runtime_error("Callee is not a function: got " + callee_val->type()); + } + return callee_val->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() == b->as_string(); + } 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() == b->as_string(); + } + // compare by type + if (a->type() != b->type()) { + return false; + } + return false; +} + } // namespace jinja diff --git a/common/jinja/jinja-vm.h b/common/jinja/jinja-vm.h index d2e763b13b..7c431cd47e 100644 --- a/common/jinja/jinja-vm.h +++ b/common/jinja/jinja-vm.h @@ -32,7 +32,7 @@ struct context { struct statement { virtual ~statement() = default; virtual std::string type() const { return "Statement"; } - virtual value execute(context & ctx) { throw std::runtime_error("cannot exec " + type()); } + virtual value execute(context &) { throw std::runtime_error("cannot exec " + type()); } }; using statement_ptr = std::unique_ptr; @@ -68,7 +68,7 @@ struct program : public statement { explicit program(statements && body) : body(std::move(body)) {} std::string type() const override { return "Program"; } - value execute(context & ctx) override { + value execute(context &) override { throw std::runtime_error("Cannot execute program directly, use jinja::vm instead"); } }; @@ -113,12 +113,30 @@ struct for_statement : public statement { struct break_statement : public statement { std::string type() const override { return "Break"; } - value execute(context & ctx) override; + + struct exception : public std::exception { + const char* what() const noexcept override { + return "Break statement executed"; + } + }; + + value execute(context &) override { + throw break_statement::exception(); + } }; struct continue_statement : public statement { std::string type() const override { return "Continue"; } - value execute(context & ctx) override; + + struct exception : public std::exception { + const char* what() const noexcept override { + return "Continue statement executed"; + } + }; + + value execute(context &) override { + throw continue_statement::exception(); + } }; struct set_statement : public statement { @@ -148,14 +166,12 @@ struct macro_statement : public statement { } std::string type() const override { return "Macro"; } - value execute(context & ctx) override {} }; struct comment_statement : public statement { std::string val; explicit comment_statement(const std::string & v) : val(v) {} std::string type() const override { return "Comment"; } - value execute(context & ctx) override {} }; // Expressions @@ -184,6 +200,7 @@ struct call_expression : public expression { for (const auto& arg : this->args) chk_type(arg); } std::string type() const override { return "CallExpression"; } + value execute(context & ctx) override; }; /** @@ -202,7 +219,7 @@ struct integer_literal : public expression { int64_t val; explicit integer_literal(int64_t val) : val(val) {} std::string type() const override { return "IntegerLiteral"; } - value execute(context & ctx) override { + value execute(context &) override { return std::make_unique(val); } }; @@ -211,7 +228,7 @@ struct float_literal : public expression { double val; explicit float_literal(double val) : val(val) {} std::string type() const override { return "FloatLiteral"; } - value execute(context & ctx) override { + value execute(context &) override { return std::make_unique(val); } }; @@ -220,7 +237,7 @@ struct string_literal : public expression { std::string val; explicit string_literal(const std::string & val) : val(val) {} std::string type() const override { return "StringLiteral"; } - value execute(context & ctx) override { + value execute(context &) override { return std::make_unique(val); } }; @@ -300,7 +317,6 @@ struct filter_statement : public statement { chk_type(this->filter); } std::string type() const override { return "FilterStatement"; } - value execute(context & ctx) override {} }; /** @@ -396,7 +412,6 @@ struct call_statement : public statement { for (const auto& arg : this->caller_args) chk_type(arg); } std::string type() const override { return "CallStatement"; } - value execute(context & ctx) override {} }; struct ternary_expression : public expression { @@ -413,6 +428,14 @@ struct ternary_expression : public expression { std::string type() const override { return "Ternary"; } }; +struct raised_exception : public std::exception { + std::string message; + raised_exception(const std::string & msg) : message(msg) {} + const char* what() const noexcept override { + return message.c_str(); + } +}; + ////////////////////// struct vm { diff --git a/tests/test-chat-jinja.cpp b/tests/test-chat-jinja.cpp index 63048841c3..085531a673 100644 --- a/tests/test-chat-jinja.cpp +++ b/tests/test-chat-jinja.cpp @@ -11,11 +11,11 @@ #include "jinja/jinja-lexer.h" int main(void) { - //std::string contents = "{% if messages[0]['role'] == 'system' %}{{ raise_exception('System role not supported') }}{% endif %}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if (message['role'] == 'assistant') %}{% set role = 'model' %}{% else %}{% set role = message['role'] %}{% endif %}{{ '' + role + '\\n' + message['content'] | trim + '\\n' }}{% endfor %}{% if add_generation_prompt %}{{'model\\n'}}{% endif %}"; + std::string contents = "{% if messages[0]['role'] == 'system' %}{{ raise_exception('System role not supported') }}{% endif %}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if (message['role'] == 'assistant') %}{% set role = 'model' %}{% else %}{% set role = message['role'] %}{% endif %}{{ '' + role + '\\n' + message['content'] | trim + '\\n' }}{% endfor %}{% if add_generation_prompt %}{{'model\\n'}}{% endif %}"; //std::string contents = "{% if messages[0]['role'] != 'system' %}nice {{ messages[0]['content'] }}{% endif %}"; - std::string contents = " {{ messages[0]['content'] }} "; + //std::string contents = " {{ messages[0]['content'] }} "; std::cout << "=== INPUT ===\n" << contents << "\n\n"; @@ -34,11 +34,11 @@ int main(void) { std::cout << "stmt type: " << stmt->type() << "\n"; } - std::cout << "\n=== OUTPUT ===\n"; + std::cout << "\n=== RUN ===\n"; jinja::context ctx; auto make_non_special_string = [](const std::string & s) { - jinja::value_string str_val = std::make_unique(s); + jinja::value_string str_val = jinja::mk_val(s); str_val->is_user_input = true; return str_val; }; @@ -57,7 +57,12 @@ int main(void) { jinja::vm vm(ctx); auto results = vm.execute(ast); + + std::cout << "\n=== RESULTS ===\n"; for (const auto & res : results) { + if (res->is_null()) { + continue; + } auto str_ptr = dynamic_cast(res.get()); std::string is_user_input = "false"; if (str_ptr) {