Commit 4f2187e73 for llama.cpp
commit 4f2187e73db10ac8b5d35bee0bdcd464331e5a1f
Author: HaoJun ZHANG <neroued@gmail.com>
Date: Sat Nov 14 20:34:20 2026 +0800
jinja : reduce template rendering overhead (#32023)
* jinja : reduce value allocation and lookup overhead
Assisted-by: GPT-6 Astra
* jinja : address rendering review feedback
Assisted-by: GPT-6 Astra
* jinja : test multipart strings and simplify lookup
Assisted-by: GPT-6 Astra
* fix "undefined hint"
* fix divisibleby 0
* clean up
---------
Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
diff --git a/common/jinja/runtime.cpp b/common/jinja/runtime.cpp
index d60a99d25..8bd8ab1c8 100644
--- a/common/jinja/runtime.cpp
+++ b/common/jinja/runtime.cpp
@@ -89,21 +89,22 @@ value statement::execute(context & ctx) const {
}
value identifier::execute_impl(context & ctx) const {
- auto it = ctx.get_val(val);
- auto builtins = global_builtins();
- if (!it->is_undefined()) {
+ auto it = ctx.find_val(key);
+ if (it && !it->is_undefined()) {
if (ctx.is_get_stats) {
value_t::stats_t::mark_used(it);
}
JJ_DEBUG("Identifier '%s' found, type = %s", val.c_str(), it->type().c_str());
return it;
- } else if (builtins.find(val) != builtins.end()) {
+ }
+ const auto & builtins = global_builtins();
+ auto builtin = builtins.find(val);
+ if (builtin != builtins.end()) {
JJ_DEBUG("Identifier '%s' found in builtins", val.c_str());
- return mk_val<value_func>(val, builtins.at(val));
- } else {
- JJ_DEBUG("Identifier '%s' not found, returning undefined", val.c_str());
- return mk_val<value_undefined>(val);
+ return mk_val<value_func>(val, builtin->second);
}
+ JJ_DEBUG("Identifier '%s' not found, returning undefined", val.c_str());
+ return mk_val<value_undefined>(val);
}
value object_literal::execute_impl(context & ctx) const {
@@ -693,7 +694,7 @@ value set_statement::execute_impl(context & ctx) const {
}
auto obj_ptr = cast_val<value_object>(object);
JJ_DEBUG("Setting object property '%s' with value type %s", prop_name.c_str(), rhs->type().c_str());
- obj_ptr->insert(prop_name, rhs);
+ obj_ptr->insert(cached_val<value_string>(ctx, member->static_property), rhs);
} else {
throw std::runtime_error("Invalid LHS inside assignment expression: " + assignee->type());
@@ -813,6 +814,19 @@ value call_statement::execute_impl(context & ctx) const {
return callee_func->invoke(args);
}
+member_expression::member_expression(statement_ptr && object, statement_ptr && property, bool computed) :
+ object(std::move(object)),
+ property(std::move(property)),
+ computed(computed) {
+ chk_type<expression>(this->object);
+ chk_type<expression>(this->property);
+ if (!computed) {
+ if (const auto * id = cast_stmt<identifier>(this->property)) {
+ static_property = id->key;
+ }
+ }
+}
+
value member_expression::execute_impl(context & ctx) const {
value object = this->object->execute(ctx);
@@ -821,14 +835,13 @@ value member_expression::execute_impl(context & ctx) const {
// syntax: obj[expr]
JJ_DEBUG("Member expression, computing property type %s", this->property->type().c_str());
- int64_t arr_size = 0;
- if (is_val<value_array>(object)) {
- arr_size = object->as_array().size();
- } else if (is_val<value_string>(object)) {
- arr_size = object->as_string().length();
- }
-
if (is_stmt<slice_expression>(this->property)) {
+ int64_t arr_size = 0;
+ if (is_val<value_array>(object)) {
+ arr_size = static_cast<int64_t>(object->as_array().size());
+ } else if (is_val<value_string>(object)) {
+ arr_size = static_cast<int64_t>(cast_val<value_string>(object)->val_str.length());
+ }
auto s = cast_stmt<slice_expression>(this->property);
value step_val = s->step_expr ? s->step_expr->execute(ctx) : mk_val<value_int>(1);
value start_val = s->start_expr ? s->start_expr->execute(ctx) : (step_val->as_int() < 0 ? mk_val<value_int>(arr_size - 1) : mk_val<value_int>(0));
@@ -850,7 +863,7 @@ value member_expression::execute_impl(context & ctx) const {
}
} else if (is_stmt<integer_literal>(this->property)) {
// syntax: obj.index
- property = mk_val<value_int>(cast_stmt<integer_literal>(this->property)->val);
+ property = this->property->execute(ctx);
if (property->as_int() < 0) {
throw std::runtime_error("Static member property cannot be negative");
}
@@ -859,8 +872,8 @@ value member_expression::execute_impl(context & ctx) const {
if (!is_stmt<identifier>(this->property)) {
throw std::runtime_error("Static member property must be an identifier");
}
- property = mk_val<value_string>(cast_stmt<identifier>(this->property)->val);
- std::string prop = property->as_string().str();
+ property = cached_val<value_string>(ctx, static_property);
+ const auto & prop = cast_stmt<identifier>(this->property)->val;
JJ_DEBUG("Member expression, object type %s, static property '%s'", object->type().c_str(), prop.c_str());
// behavior of jinja2: obj having prop as a built-in function AND 'prop', as an object key,
@@ -868,51 +881,56 @@ value member_expression::execute_impl(context & ctx) const {
// while obj['prop'] returns the property value.
// example: {"obj": {"items": 123}} -> obj.items is the built-in function, obj['items'] is 123
- value val = try_builtin_func(ctx, prop, object, true);
- if (!is_val<value_undefined>(val)) {
+ value val = find_builtin_func(ctx, prop, object);
+ if (val) {
return val;
}
// else, fallthrough to normal property access below
}
JJ_DEBUG("Member expression on object type %s, property type %s", object->type().c_str(), property->type().c_str());
- value val = mk_val<value_undefined>("object_property");
+ value val;
if (property->is_undefined()) {
JJ_DEBUG("%s", "Member expression property is undefined, returning undefined");
- return val;
+ return mk_val<value_undefined>("object_property");
}
ensure_key_type_allowed(property);
if (is_val<value_undefined>(object)) {
JJ_DEBUG("%s", "Accessing property on undefined object, returning undefined");
- return val;
+ return mk_val<value_undefined>("object_property");
} else if (is_val<value_object>(object)) {
- auto key = property->as_string().str();
- val = object->at(property, val);
- if (is_val<value_undefined>(val)) {
- val = try_builtin_func(ctx, key, object, true);
+ val = object->find(property);
+ if (!val || is_val<value_undefined>(val)) {
+ val = try_builtin_func(ctx, property->as_string().str(), object, true);
}
- JJ_DEBUG("Accessed property '%s' value, got type: %s", key.c_str(), val->type().c_str());
+ JJ_DEBUG("Accessed property '%s' value, got type: %s", property->as_string().str().c_str(), val->type().c_str());
} else if (is_val<value_array>(object) || is_val<value_string>(object)) {
if (is_val<value_int>(property) || is_val<value_bool>(property)) {
int64_t index = property->as_int();
JJ_DEBUG("Accessing %s index %d", object->type().c_str(), (int)index);
if (is_val<value_array>(object)) {
- auto & arr = object->as_array();
+ const auto * arr = cast_val<value_array>(object);
+ const auto count = arr->as_array().size();
if (index < 0) {
- index += static_cast<int64_t>(arr.size());
- }
- if (index >= 0 && index < static_cast<int64_t>(arr.size())) {
- val = arr[index];
+ // Avoid negating INT64_MIN.
+ const uint64_t distance = static_cast<uint64_t>(-(index + 1)) + 1;
+ if (distance <= count) {
+ val = arr->as_array().at(static_cast<size_t>(count - distance));
+ }
+ } else if (static_cast<uint64_t>(index) < count) {
+ val = arr->as_array().at(static_cast<size_t>(index));
}
} else { // value_string
- auto str = object->as_string().str();
- if (index >= 0 && index < static_cast<int64_t>(str.size())) {
- val = mk_val<value_string>(std::string(1, str[index]));
+ if (index >= 0) {
+ const auto byte = cast_val<value_string>(object)->val_str.byte_at(static_cast<size_t>(index));
+ if (byte) {
+ val = mk_val<value_string>(std::string(1, *byte));
+ }
}
}
@@ -930,7 +948,10 @@ value member_expression::execute_impl(context & ctx) const {
val = try_builtin_func(ctx, key, object, true);
}
- if (ctx.is_get_stats && val && object && property) {
+ if (!val) {
+ val = mk_val<value_undefined>("object_property");
+ }
+ if (ctx.is_get_stats && object && property) {
value_t::stats_t::mark_used(val);
value_t::stats_t::mark_used(object);
value_t::stats_t::mark_used(property);
diff --git a/common/jinja/runtime.h b/common/jinja/runtime.h
index 43e108551..97db5de33 100644
--- a/common/jinja/runtime.h
+++ b/common/jinja/runtime.h
@@ -84,9 +84,14 @@ struct context {
// inherit variables (for example, when entering a new scope)
env(mk_val<value_object>(*parent.env)) {}
+ // returns nullptr if not found
+ value find_val(const value & key) {
+ return env->find(key);
+ }
+
value get_val(const std::string & name) {
- value default_val = mk_val<value_undefined>(name);
- return env->at(name, default_val);
+ auto result = find_val(mk_val<value_string>(name));
+ return result ? result : mk_val<value_undefined>(name);
}
void set_val(const std::string & name, const value & val) {
@@ -105,6 +110,12 @@ private:
value_object env;
};
+// values cached in the AST are shared across renders, but stats mutate values, so stats mode gets a fresh copy
+template<typename T>
+value cached_val(const context & ctx, const value & cached) {
+ return ctx.is_get_stats ? mk_val<T>(*cast_val<T>(cached)) : cached;
+}
+
// utils for visiting AST nodes
static std::vector<const statement *> stmts_to_ptr(const statements & stmts) {
std::vector<const statement *> children;
@@ -328,11 +339,8 @@ struct member_expression : public expression {
statement_ptr property;
bool computed; // true if obj[expr] and false if obj.prop
- member_expression(statement_ptr && object, statement_ptr && property, bool computed)
- : object(std::move(object)), property(std::move(property)), computed(computed) {
- chk_type<expression>(this->object);
- chk_type<expression>(this->property);
- }
+ value static_property; // set if property is an identifier, see cached_val()
+ member_expression(statement_ptr && object, statement_ptr && property, bool computed);
std::string type() const override { return "MemberExpression"; }
value execute_impl(context & ctx) const override;
void visit(context & ctx) const override {
@@ -367,7 +375,8 @@ struct call_expression : public expression {
*/
struct identifier : public expression {
std::string val;
- explicit identifier(const std::string & val) : val(val) {}
+ const value key; // lookup key, safe to share across renders as lookups do not touch stats
+ explicit identifier(const std::string & val) : val(val), key(mk_val<value_string>(val)) {}
std::string type() const override { return "Identifier"; }
value execute_impl(context & ctx) const override;
};
@@ -376,28 +385,31 @@ struct identifier : public expression {
struct integer_literal : public expression {
int64_t val;
- explicit integer_literal(int64_t val) : val(val) {}
+ const value constant; // see cached_val()
+ explicit integer_literal(int64_t val) : val(val), constant(mk_val<value_int>(val)) {}
std::string type() const override { return "IntegerLiteral"; }
- value execute_impl(context &) const override {
- return mk_val<value_int>(val);
+ value execute_impl(context & ctx) const override {
+ return cached_val<value_int>(ctx, constant);
}
};
struct float_literal : public expression {
double val;
- explicit float_literal(double val) : val(val) {}
+ const value constant; // see cached_val()
+ explicit float_literal(double val) : val(val), constant(mk_val<value_float>(val)) {}
std::string type() const override { return "FloatLiteral"; }
- value execute_impl(context &) const override {
- return mk_val<value_float>(val);
+ value execute_impl(context & ctx) const override {
+ return cached_val<value_float>(ctx, constant);
}
};
struct string_literal : public expression {
std::string val;
- explicit string_literal(const std::string & val) : val(val) {}
+ const value constant; // see cached_val()
+ explicit string_literal(const std::string & val) : val(val), constant(mk_val<value_string>(val)) {}
std::string type() const override { return "StringLiteral"; }
- value execute_impl(context &) const override {
- return mk_val<value_string>(val);
+ value execute_impl(context & ctx) const override {
+ return cached_val<value_string>(ctx, constant);
}
};
@@ -748,7 +760,7 @@ static void gather_string_parts_recursive(const value & val, value_string & part
} else if (is_val<value_array>(val) && depth >= JINJA_RECURSIVE_LIMIT) {
throw std::runtime_error("value is nested more than " + std::to_string(JINJA_RECURSIVE_LIMIT) + " levels deep");
} else if (is_val<value_array>(val)) {
- auto items = cast_val<value_array>(val)->as_array();
+ const auto & items = cast_val<value_array>(val)->as_array();
for (const auto & item : items) {
gather_string_parts_recursive(item, parts, depth + 1);
}
diff --git a/common/jinja/string.cpp b/common/jinja/string.cpp
index bde679e4e..22189fa6f 100644
--- a/common/jinja/string.cpp
+++ b/common/jinja/string.cpp
@@ -43,14 +43,59 @@ void string::mark_input() {
}
std::string string::str() const {
- if (parts.size() == 1) {
- return parts[0].val;
+ std::string result;
+ result.reserve(length());
+ for (const auto & part : parts) {
+ result += part.val;
+ }
+ return result;
+}
+
+bool string::empty() const {
+ for (const auto & part : parts) {
+ if (!part.val.empty()) {
+ return false;
+ }
}
- std::ostringstream oss;
+ return true;
+}
+
+std::optional<char> string::byte_at(size_t index) const {
for (const auto & part : parts) {
- oss << part.val;
+ if (index < part.val.size()) {
+ return part.val[index];
+ }
+ index -= part.val.size();
+ }
+ return {};
+}
+
+bool string::equals(const string & other) const {
+ size_t part = 0, offset = 0;
+ for (const auto & left : parts) {
+ std::string_view text(left.val);
+ while (!text.empty()) {
+ while (part < other.parts.size() && offset == other.parts[part].val.size()) {
+ ++part;
+ offset = 0;
+ }
+ if (part == other.parts.size()) {
+ return false;
+ }
+ const auto & right = other.parts[part].val;
+ const auto count = std::min(text.size(), right.size() - offset);
+ if (text.substr(0, count) != std::string_view(right).substr(offset, count)) {
+ return false;
+ }
+ text.remove_prefix(count);
+ offset += count;
+ }
+ }
+ while (part < other.parts.size() && offset == other.parts[part].val.size()) {
+ ++part;
+ offset = 0;
}
- return oss.str();
+ return part == other.parts.size();
}
size_t string::length() const {
diff --git a/common/jinja/string.h b/common/jinja/string.h
index 669afb8f1..119ad11c2 100644
--- a/common/jinja/string.h
+++ b/common/jinja/string.h
@@ -39,6 +39,9 @@ struct string {
std::string str() const;
size_t length() const;
+ bool empty() const;
+ bool equals(const string & other) const;
+ std::optional<char> byte_at(size_t index) const;
void hash_update(hasher & hash) const noexcept;
bool all_parts_are_input() const;
bool is_uppercase() const;
diff --git a/common/jinja/value.cpp b/common/jinja/value.cpp
index 333241d7a..561a4b86a 100644
--- a/common/jinja/value.cpp
+++ b/common/jinja/value.cpp
@@ -276,7 +276,7 @@ static value tojson(const func_args & args) {
if (args.ctx.is_get_stats) {
// mark as used (recursively) for stats
auto val_input = args.get_pos(0);
- value_t::stats_t::mark_used(const_cast<value&>(val_input), true);
+ value_t::stats_t::mark_used(val_input, true);
}
if (is_val<value_int>(val_indent)) {
indent = static_cast<int>(val_indent->as_int());
@@ -507,7 +507,12 @@ const func_builtins & global_builtins() {
}},
{"test_is_divisibleby", [](const func_args & args) -> value {
args.ensure_vals<value_int, value_int>();
- bool res = args.get_pos(0)->val_int % args.get_pos(1)->val_int == 0;
+ const int64_t a = args.get_pos(0)->as_int();
+ const int64_t b = args.get_pos(1)->as_int();
+ if (b == 0) {
+ throw std::runtime_error("divisibleby: division by zero");
+ }
+ bool res = b == -1 || a % b == 0; // avoid INT64_MIN % -1 overflow
return mk_val<value_bool>(res);
}},
{"test_is_string", test_type_fn<value_string>},
@@ -519,11 +524,11 @@ const func_builtins & global_builtins() {
{"test_is_mapping", test_type_fn<value_object>},
{"test_is_lower", [](const func_args & args) -> value {
args.ensure_vals<value_string>();
- return mk_val<value_bool>(args.get_pos(0)->val_str.is_lowercase());
+ return mk_val<value_bool>(cast_val<value_string>(args.get_pos(0))->val_str.is_lowercase());
}},
{"test_is_upper", [](const func_args & args) -> value {
args.ensure_vals<value_string>();
- return mk_val<value_bool>(args.get_pos(0)->val_str.is_uppercase());
+ return mk_val<value_bool>(cast_val<value_string>(args.get_pos(0))->val_str.is_uppercase());
}},
{"test_is_none", test_type_fn<value_none>},
{"test_is_defined", [](const func_args & args) -> value {
@@ -571,7 +576,7 @@ const func_builtins & global_builtins() {
{"test_is_test", [](const func_args & args) -> value {
args.ensure_vals<value_string>();
auto & builtins = global_builtins();
- std::string test_name = args.get_pos(0)->val_str.str();
+ std::string test_name = cast_val<value_string>(args.get_pos(0))->val_str.str();
auto it = builtins.find("test_is_" + test_name);
bool res = it != builtins.end();
return mk_val<value_bool>(res);
@@ -613,18 +618,26 @@ const func_builtins & global_builtins() {
return builtins;
}
-value try_builtin_func(context & ctx, const std::string & name, value & input, bool undef_on_missing) {
+value find_builtin_func(context & ctx, const std::string & name, value & input) {
JJ_DEBUG("Trying built-in function '%s' for type %s", name.c_str(), input->type().c_str());
if (ctx.is_get_stats) {
value_t::stats_t::mark_used(input);
input->stats.ops.insert(name);
}
- auto builtins = input->get_builtins();
+ const 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<value_func>(name, it->second, input);
}
+ return nullptr;
+}
+
+value try_builtin_func(context & ctx, const std::string & name, value & input, bool undef_on_missing) {
+ value func = find_builtin_func(ctx, name, input);
+ if (func) {
+ return func;
+ }
if (undef_on_missing) {
return mk_val<value_undefined>(name);
}
@@ -831,7 +844,7 @@ const func_builtins & value_string_t::get_builtins() const {
--maxsplit;
}
auto res = mk_val<value_string>(str);
- res->val_str.mark_input_based_on(args.get_pos(0)->val_str);
+ res->val_str.mark_input_based_on(cast_val<value_string>(args.get_pos(0))->val_str);
result->push_back(std::move(res));
return result;
}},
@@ -858,7 +871,7 @@ const func_builtins & value_string_t::get_builtins() const {
--maxsplit;
}
auto res = mk_val<value_string>(str);
- res->val_str.mark_input_based_on(args.get_pos(0)->val_str);
+ res->val_str.mark_input_based_on(cast_val<value_string>(args.get_pos(0))->val_str);
result->push_back(std::move(res));
result->reverse();
return result;
@@ -891,7 +904,7 @@ const func_builtins & value_string_t::get_builtins() const {
}
}
auto res = mk_val<value_string>(str);
- res->val_str.mark_input_based_on(args.get_pos(0)->val_str);
+ res->val_str.mark_input_based_on(cast_val<value_string>(args.get_pos(0))->val_str);
return res;
}},
{"format", [](const func_args & args) -> value {
@@ -1191,7 +1204,7 @@ const func_builtins & value_array_t::get_builtins() const {
if (args.ctx.is_get_stats) {
// mark as used (recursively) for stats
auto val_input = args.get_pos(0);
- value_t::stats_t::mark_used(const_cast<value&>(val_input), true);
+ value_t::stats_t::mark_used(val_input, true);
}
return mk_val<value_string>(args.get_pos(0)->as_string());
}},
@@ -1439,7 +1452,7 @@ const func_builtins & value_object_t::get_builtins() const {
if (args.ctx.is_get_stats) {
// mark as used (recursively) for stats
auto val_input = args.get_pos(0);
- value_t::stats_t::mark_used(const_cast<value&>(val_input), true);
+ value_t::stats_t::mark_used(val_input, true);
}
return mk_val<value_string>(args.get_pos(0)->as_string());
}},
@@ -1833,18 +1846,18 @@ std::string value_to_string_repr(const value & val) {
}
// stats utility
-void value_t::stats_t::mark_used(value & val, bool deep, int depth) {
+void value_t::stats_t::mark_used(const value & val, bool deep, int depth) {
val->stats.used = true;
if (deep && (is_val<value_array>(val) || is_val<value_object>(val)) && depth >= JINJA_RECURSIVE_LIMIT) {
throw std::runtime_error("value is nested more than " + std::to_string(JINJA_RECURSIVE_LIMIT) + " levels deep");
}
if (deep) {
if (is_val<value_array>(val)) {
- for (auto & item : val->val_arr) {
+ for (auto & item : val->as_array()) {
mark_used(item, deep, depth + 1);
}
} else if (is_val<value_object>(val)) {
- for (auto & pair : val->val_obj) {
+ for (auto & pair : val->as_ordered_object()) {
mark_used(pair.first, deep, depth + 1);
mark_used(pair.second, deep, depth + 1);
}
diff --git a/common/jinja/value.h b/common/jinja/value.h
index 8fdff884e..bdc35ed39 100644
--- a/common/jinja/value.h
+++ b/common/jinja/value.h
@@ -21,6 +21,20 @@ namespace jinja {
struct value_t;
using value = std::shared_ptr<value_t>;
+enum class value_kind {
+ INTEGER,
+ REAL,
+ STRING,
+ BOOLEAN,
+ ARRAY,
+ TUPLE,
+ OBJECT,
+ NONE,
+ UNDEFINED,
+ FUNCTION,
+ KEYWORD
+};
+bool has_value_kind(const value_t * val, value_kind kind);
// Helper to check the type of a value
template<typename T>
@@ -34,12 +48,12 @@ struct extract_pointee<std::shared_ptr<U>> {
template<typename T>
bool is_val(const value & ptr) {
using PointeeType = typename extract_pointee<T>::type;
- return dynamic_cast<const PointeeType*>(ptr.get()) != nullptr;
+ return has_value_kind(ptr.get(), PointeeType::kind_id);
}
template<typename T>
bool is_val(const value_t * ptr) {
using PointeeType = typename extract_pointee<T>::type;
- return dynamic_cast<const PointeeType*>(ptr) != nullptr;
+ return has_value_kind(ptr, PointeeType::kind_id);
}
template<typename T, typename... Args>
std::shared_ptr<typename extract_pointee<T>::type> mk_val(Args&&... args) {
@@ -49,12 +63,12 @@ std::shared_ptr<typename extract_pointee<T>::type> mk_val(Args&&... args) {
template<typename T>
const typename extract_pointee<T>::type * cast_val(const value & ptr) {
using PointeeType = typename extract_pointee<T>::type;
- return dynamic_cast<const PointeeType*>(ptr.get());
+ return is_val<T>(ptr) ? static_cast<const PointeeType *>(ptr.get()) : nullptr;
}
template<typename T>
typename extract_pointee<T>::type * cast_val(value & ptr) {
using PointeeType = typename extract_pointee<T>::type;
- return dynamic_cast<PointeeType*>(ptr.get());
+ return is_val<T>(ptr) ? static_cast<PointeeType *>(ptr.get()) : nullptr;
}
// End Helper
@@ -105,14 +119,7 @@ enum value_compare_op { eq, ge, gt, lt, ne };
bool value_compare(const value & a, const value & b, value_compare_op op);
struct value_t {
- int64_t val_int;
- double val_flt;
- string val_str;
-
- std::vector<value> val_arr;
- std::vector<std::pair<value, value>> val_obj;
-
- func_handler val_func;
+ const value_kind kind;
// only used if ctx.is_get_stats = true
struct stats_t {
@@ -120,10 +127,10 @@ struct value_t {
// ops can be builtin calls or operators: "array_access", "object_access"
std::set<std::string> ops;
// utility to recursively mark value and its children as used
- static void mark_used(value & val, bool deep = false, int depth = 0);
+ static void mark_used(const value & val, bool deep = false, int depth = 0);
} stats;
- value_t() = default;
+ explicit value_t(value_kind kind) : kind(kind) {}
value_t(const value_t &) = default;
virtual ~value_t() = default;
@@ -141,6 +148,10 @@ struct value_t {
virtual bool is_undefined() const { return false; }
virtual const func_builtins & get_builtins() const { throw_type_error("has no builtins"); }
+ virtual value find(const value & key) {
+ value missing;
+ return at(key, missing);
+ }
virtual bool has_key(const value &) { throw_type_error("is not an object value"); }
virtual void insert(const value & /* key */, const value & /* val */) { throw_type_error("is not an object value"); }
virtual value & at(const value & /* key */, value & /* default_val */) { throw_type_error("is not an object value"); }
@@ -172,11 +183,19 @@ protected:
virtual bool nonequal(const value_t & other) const { return !equivalent(other); }
};
+inline bool has_value_kind(const value_t * val, value_kind kind) {
+ return val && (val->kind == kind ||
+ (kind == value_kind::ARRAY && (val->kind == value_kind::TUPLE)));
+}
+
//
// utils
//
const func_builtins & global_builtins();
+// returns nullptr if input has no built-in named `name`
+value find_builtin_func(context & ctx, const std::string & name, value & input);
+// returns value_undefined(name) if not found and undef_on_missing is set, throws otherwise
value try_builtin_func(context & ctx, const std::string & name, value & input, bool undef_on_missing = false);
value apply_filter_by_name(context & ctx, std::string filter_id, value input, const func_args & args);
@@ -215,7 +234,10 @@ struct value_equality {
//
struct value_int_t : public value_t {
- value_int_t(int64_t v) {
+ static constexpr auto kind_id = value_kind::INTEGER;
+ int64_t val_int;
+ double val_flt;
+ value_int_t(int64_t v) : value_t(kind_id) {
val_int = v;
val_flt = static_cast<double>(v);
if (static_cast<int64_t>(val_flt) != v) {
@@ -239,21 +261,22 @@ struct value_int_t : public value_t {
}
protected:
virtual bool equivalent(const value_t & other) const override {
- return other.is_numeric() && val_int == other.val_int && val_flt == other.val_flt;
+ return other.is_numeric() && val_int == other.as_int() && val_flt == other.as_float();
}
virtual bool nonequal(const value_t & other) const override {
- return !(typeid(*this) == typeid(other) && val_int == other.val_int);
+ return !(kind == other.kind && val_int == other.as_int());
}
};
using value_int = std::shared_ptr<value_int_t>;
struct value_float_t : public value_t {
- value val;
- value_float_t(double v) {
+ static constexpr auto kind_id = value_kind::REAL;
+ int64_t val_int;
+ double val_flt;
+ value_float_t(double v) : value_t(kind_id) {
val_flt = v;
val_int = std::isfinite(v) ? static_cast<int64_t>(v) : 0;
- val = mk_val<value_int>(val_int);
}
virtual std::string type() const override { return "Float"; }
virtual double as_float() const override { return val_flt; }
@@ -272,7 +295,7 @@ struct value_float_t : public value_t {
virtual bool is_hashable() const override { return true; }
virtual hasher unique_hash() const noexcept override {
if (static_cast<double>(val_int) == val_flt) {
- return val->unique_hash();
+ return value_int_t(val_int).unique_hash();
} else {
return hasher(typeid(*this))
.update(&val_int, sizeof(val_int))
@@ -281,19 +304,22 @@ struct value_float_t : public value_t {
}
protected:
virtual bool equivalent(const value_t & other) const override {
- return other.is_numeric() && val_int == other.val_int && val_flt == other.val_flt;
+ return other.is_numeric() && val_int == other.as_int() && val_flt == other.as_float();
}
virtual bool nonequal(const value_t & other) const override {
- return !(typeid(*this) == typeid(other) && val_flt == other.val_flt);
+ return !(kind == other.kind && val_flt == other.as_float());
}
};
using value_float = std::shared_ptr<value_float_t>;
struct value_string_t : public value_t {
- value_string_t() { val_str = string(); }
- value_string_t(const std::string & v) { val_str = string(v); }
- value_string_t(const string & v) { val_str = v; }
+ static constexpr auto kind_id = value_kind::STRING;
+ string val_str;
+ value_string_t() : value_t(kind_id) {}
+ value_string_t(const std::string & v) : value_t(kind_id), val_str(v) {}
+ value_string_t(const string & v) : value_t(kind_id), val_str(v) {}
+ value_string_t(string && v) : value_t(kind_id), val_str(std::move(v)) {}
virtual std::string type() const override { return "String"; }
virtual string as_string() const override { return val_str; }
virtual std::string as_repr() const override {
@@ -304,7 +330,7 @@ struct value_string_t : public value_t {
return ss.str();
}
virtual bool as_bool() const override {
- return val_str.length() > 0;
+ return !val_str.empty();
}
virtual const func_builtins & get_builtins() const override;
virtual bool is_hashable() const override { return true; }
@@ -320,51 +346,50 @@ struct value_string_t : public value_t {
}
protected:
virtual bool equivalent(const value_t & other) const override {
- return typeid(*this) == typeid(other) && val_str.str() == other.val_str.str();
+ return kind == other.kind && val_str.equals(static_cast<const value_string_t &>(other).val_str);
}
};
using value_string = std::shared_ptr<value_string_t>;
struct value_bool_t : public value_t {
- value val;
- value_bool_t(bool v) {
+ static constexpr auto kind_id = value_kind::BOOLEAN;
+ int64_t val_int;
+ double val_flt;
+ value_bool_t(bool v) : value_t(kind_id) {
val_int = static_cast<int64_t>(v);
val_flt = static_cast<double>(v);
- val = mk_val<value_int>(val_int);
}
virtual std::string type() const override { return "Boolean"; }
virtual int64_t as_int() const override { return val_int; }
+ virtual double as_float() const override { return val_flt; }
virtual bool as_bool() const override { return val_int; }
virtual string as_string() const override { return std::string(val_int ? "True" : "False"); }
virtual const func_builtins & get_builtins() const override;
virtual bool is_numeric() const override { return true; }
virtual bool is_hashable() const override { return true; }
virtual hasher unique_hash() const noexcept override {
- return val->unique_hash();
+ return value_int_t(val_int).unique_hash();
}
protected:
virtual bool equivalent(const value_t & other) const override {
- return other.is_numeric() && val_int == other.val_int && val_flt == other.val_flt;
+ return other.is_numeric() && val_int == other.as_int() && val_flt == other.as_float();
}
virtual bool nonequal(const value_t & other) const override {
- return !(typeid(*this) == typeid(other) && val_int == other.val_int);
+ return !(kind == other.kind && val_int == other.as_int());
}
};
using value_bool = std::shared_ptr<value_bool_t>;
struct value_array_t : public value_t {
- value_array_t() = default;
- value_array_t(value & v) {
- val_arr = v->val_arr;
- }
- value_array_t(std::vector<value> && arr) {
- val_arr = arr;
- }
- value_array_t(const std::vector<value> & arr) {
- val_arr = arr;
- }
+ static constexpr auto kind_id = value_kind::ARRAY;
+ std::vector<value> val_arr;
+
+ value_array_t() : value_t(kind_id) {}
+ value_array_t(value & v) : value_t(kind_id), val_arr(v->as_array()) {}
+ value_array_t(std::vector<value> && arr) : value_t(kind_id), val_arr(std::move(arr)) {}
+ value_array_t(const std::vector<value> & arr) : value_t(kind_id), val_arr(arr) {}
void reverse() {
if (is_immutable()) {
throw std::runtime_error("Attempting to modify immutable type");
@@ -462,24 +487,27 @@ struct value_array_t : public value_t {
return hash;
}
protected:
+ explicit value_array_t(value_kind kind) : value_t(kind) {}
+
virtual bool equivalent(const value_t & other) const override {
- return typeid(*this) == typeid(other) && is_hashable() && other.is_hashable() && std::equal(val_arr.begin(), val_arr.end(), other.val_arr.begin(), other.val_arr.end(), value_equivalence());
+ return kind == other.kind && is_hashable() && other.is_hashable() && std::equal(val_arr.begin(), val_arr.end(), other.as_array().begin(), other.as_array().end(), value_equivalence());
}
};
using value_array = std::shared_ptr<value_array_t>;
struct value_tuple_t : public value_array_t {
- value_tuple_t(value & v) {
- val_arr = v->val_arr;
+ static constexpr auto kind_id = value_kind::TUPLE;
+ value_tuple_t(value & v) : value_array_t(kind_id) {
+ val_arr = v->as_array();
}
- value_tuple_t(std::vector<value> && arr) {
- val_arr = arr;
+ value_tuple_t(std::vector<value> && arr) : value_array_t(kind_id) {
+ val_arr = std::move(arr);
}
- value_tuple_t(const std::vector<value> & arr) {
+ value_tuple_t(const std::vector<value> & arr) : value_array_t(kind_id) {
val_arr = arr;
}
- value_tuple_t(const std::pair<value, value> & pair) {
+ value_tuple_t(const std::pair<value, value> & pair) : value_array_t(kind_id) {
val_arr.push_back(pair.first);
val_arr.push_back(pair.second);
}
@@ -490,21 +518,23 @@ using value_tuple = std::shared_ptr<value_tuple_t>;
struct value_object_t : public value_t {
- std::unordered_map<value, value, value_hasher, value_equivalence> unordered;
+ static constexpr auto kind_id = value_kind::OBJECT;
+ std::vector<std::pair<value, value>> val_obj;
+ std::unordered_map<value, size_t, value_hasher, value_equivalence> key_index; // key -> index in val_obj
bool has_builtins = true; // context and loop objects do not have builtins
- value_object_t() = default;
- value_object_t(value & v) {
- val_obj = v->val_obj;
- for (const auto & pair : val_obj) {
- unordered[pair.first] = pair.second;
+ value_object_t() : value_t(kind_id) {}
+ value_object_t(value & v) : value_t(kind_id) {
+ val_obj = v->as_ordered_object();
+ for (size_t i = 0; i < val_obj.size(); i++) {
+ key_index[val_obj[i].first] = i;
}
}
- value_object_t(const std::map<value, value> & obj) {
+ value_object_t(const std::map<value, value> & obj) : value_t(kind_id) {
for (const auto & pair : obj) {
insert(pair.first, pair.second);
}
}
- value_object_t(const std::vector<std::pair<value, value>> & obj) {
+ value_object_t(const std::vector<std::pair<value, value>> & obj) : value_t(kind_id) {
for (const auto & pair : obj) {
insert(pair.first, pair.second);
}
@@ -527,45 +557,40 @@ struct value_object_t : public value_t {
return ss.str();
}
virtual bool as_bool() const override {
- return !unordered.empty();
+ return !val_obj.empty();
}
virtual bool has_key(const value & key) override {
if (!key->is_immutable() || !key->is_hashable()) {
throw std::runtime_error("Object key of unhashable type: " + key->type());
}
- return unordered.find(key) != unordered.end();
+ return key_index.find(key) != key_index.end();
}
virtual void insert(const value & key, const value & val) override {
- bool replaced = false;
if (is_immutable()) {
throw std::runtime_error("Attempting to modify immutable type");
}
- if (has_key(key)) {
- // if key exists, replace value in ordered list instead of appending
- for (auto & pair : val_obj) {
- if (*(pair.first) == *key) {
- pair.second = val;
- replaced = true;
- break;
- }
- }
- }
- unordered[key] = val;
- if (!replaced) {
+ if (!key->is_immutable() || !key->is_hashable()) {
+ throw std::runtime_error("Object key of unhashable type: " + key->type());
+ }
+ const auto entry = key_index.try_emplace(key, val_obj.size());
+ if (entry.second) {
val_obj.push_back({key, val});
+ } else {
+ val_obj[entry.first->second].second = val;
}
}
virtual value & at(const value & key, value & default_val) override {
- if (!has_key(key)) {
- return default_val;
+ if (!key->is_immutable() || !key->is_hashable()) {
+ throw std::runtime_error("Object key of unhashable type: " + key->type());
}
- return unordered.at(key);
+ const auto found = key_index.find(key);
+ return found == key_index.end() ? default_val : val_obj[found->second].second;
}
virtual value & at(const value & key) override {
if (!has_key(key)) {
throw std::runtime_error("Key '" + key->as_string().str() + "' not found in value of type " + type());
}
- return unordered.at(key);
+ return val_obj[key_index.at(key)].second;
}
virtual value & at(const std::string & key, value & default_val) override {
value key_val = mk_val<value_string>(key);
@@ -599,7 +624,7 @@ struct value_object_t : public value_t {
}
protected:
virtual bool equivalent(const value_t & other) const override {
- return typeid(*this) == typeid(other) && is_hashable() && other.is_hashable() && std::equal(val_obj.begin(), val_obj.end(), other.val_obj.begin(), other.val_obj.end(), value_equivalence());
+ return kind == other.kind && is_hashable() && other.is_hashable() && std::equal(val_obj.begin(), val_obj.end(), other.as_ordered_object().begin(), other.as_ordered_object().end(), value_equivalence());
}
};
using value_object = std::shared_ptr<value_object_t>;
@@ -609,6 +634,8 @@ using value_object = std::shared_ptr<value_object_t>;
//
struct value_none_t : public value_t {
+ static constexpr auto kind_id = value_kind::NONE;
+ value_none_t() : value_t(kind_id) {}
virtual std::string type() const override { return "None"; }
virtual bool is_none() const override { return true; }
virtual bool as_bool() const override { return false; }
@@ -621,14 +648,15 @@ struct value_none_t : public value_t {
}
protected:
virtual bool equivalent(const value_t & other) const override {
- return typeid(*this) == typeid(other);
+ return kind == other.kind;
}
};
using value_none = std::shared_ptr<value_none_t>;
struct value_undefined_t : public value_t {
+ static constexpr auto kind_id = value_kind::UNDEFINED;
std::string hint; // for debugging, to indicate where undefined came from
- value_undefined_t(const std::string & h = "") : hint(h) {}
+ value_undefined_t(const std::string & h = "") : value_t(kind_id), hint(h) {}
virtual std::string type() const override { return hint.empty() ? "Undefined" : "Undefined (hint: '" + hint + "')"; }
virtual bool is_undefined() const override { return true; }
virtual bool as_bool() const override { return false; }
@@ -705,12 +733,14 @@ private:
};
struct value_func_t : public value_t {
+ static constexpr auto kind_id = value_kind::FUNCTION;
+ func_handler val_func;
std::string name;
value arg0; // bound "this" argument, if any
- value_func_t(const std::string & name, const func_handler & func) : name(name) {
+ value_func_t(const std::string & name, const func_handler & func) : value_t(kind_id), name(name) {
val_func = func;
}
- value_func_t(const std::string & name, const func_handler & func, const value & arg_this) : name(name), arg0(arg_this) {
+ value_func_t(const std::string & name, const func_handler & func, const value & arg_this) : value_t(kind_id), name(name), arg0(arg_this) {
val_func = func;
}
virtual value invoke(const func_args & args) const override {
@@ -735,18 +765,22 @@ protected:
// Note: this is unused for now, we don't support function as object keys
// compare function pointers
// (val_func == other.val_func does not work as std::function::operator== is only used for nullptr check)
+ if (kind != other.kind) {
+ return false;
+ }
const auto target_this = this->val_func.target<func_hptr>();
- const auto target_other = other.val_func.target<func_hptr>();
- return typeid(*this) == typeid(other) && target_this == target_other;
+ const auto target_other = static_cast<const value_func_t &>(other).val_func.target<func_hptr>();
+ return target_this == target_other;
}
};
using value_func = std::shared_ptr<value_func_t>;
// special value for kwarg
struct value_kwarg_t : public value_t {
+ static constexpr auto kind_id = value_kind::KEYWORD;
std::string key;
value val;
- value_kwarg_t(const std::string & k, const value & v) : key(k), val(v) {}
+ value_kwarg_t(const std::string & k, const value & v) : value_t(kind_id), key(k), val(v) {}
virtual std::string type() const override { return "KwArg"; }
virtual std::string as_repr() const override { return type(); }
virtual bool is_hashable() const override { return true; }
@@ -760,7 +794,7 @@ struct value_kwarg_t : public value_t {
protected:
virtual bool equivalent(const value_t & other) const override {
const value_kwarg_t & other_val = static_cast<const value_kwarg_t &>(other);
- return typeid(*this) == typeid(other) && key == other_val.key && val == other_val.val;
+ return kind == other.kind && key == other_val.key && val == other_val.val;
}
};
using value_kwarg = std::shared_ptr<value_kwarg_t>;
diff --git a/tests/test-jinja.cpp b/tests/test-jinja.cpp
index c79b0d24b..d52f1af9c 100644
--- a/tests/test-jinja.cpp
+++ b/tests/test-jinja.cpp
@@ -2,6 +2,7 @@
#include <iostream>
#include <random>
#include <cstdlib>
+#include <future>
#include "chat-tts-norm.h"
#include "json.h"
@@ -42,6 +43,8 @@ static void test_string_parts(testing & t);
static void test_fuzzing(testing & t);
static void test_tts_norm_patterns(testing & t);
+static void test_shared_render(testing & t);
+
static bool g_python_mode = false;
int main(int argc, char *argv[]) {
@@ -79,6 +82,7 @@ int main(int argc, char *argv[]) {
t.test("object methods", test_object_methods);
t.test("tts norm patterns", test_tts_norm_patterns);
if (!g_python_mode) {
+ t.test("shared render", test_shared_render);
t.test("hasher", test_hasher);
t.test("stats", test_stats);
t.test("caps", test_caps);
@@ -89,6 +93,45 @@ int main(int argc, char *argv[]) {
return t.summary();
}
+static void test_shared_render(testing & t) {
+ t.test("shared AST with stats and input marking", [](testing & t) {
+ const std::string source = "{% macro emit(x) %}{{ x }}{% endmacro %}{% for i in range(2) %}{{ emit(prefix) }}{{ i }}{% endfor %}{% filter upper %}{{ body }}{% endfilter %}|{{ 'literal' is string }}|{{ obj.value }}";
+ const auto program = jinja::parse_from_tokens(jinja::lexer().tokenize(source));
+ std::vector<std::future<bool>> workers;
+ for (int worker = 0; worker < 8; ++worker) {
+ workers.push_back(std::async(std::launch::async, [&, worker] {
+ const auto prefix = "p" + std::to_string(worker);
+ for (int i = 0; i < 25; ++i) {
+ jinja::context ctx(source);
+ ctx.is_get_stats = i % 2 != 0;
+ const json input = {
+ { "prefix", prefix },
+ { "body", "hi" },
+ { "obj", { { "value", "V" } } }
+ };
+ jinja::global_from_json(ctx, input, true);
+ const auto result = jinja::runtime::gather_string_parts(jinja::runtime(ctx).execute(program));
+ if (result->as_string().str() != prefix + "0" + prefix + "1HI|True|V") {
+ return false;
+ }
+ if (ctx.get_val("obj")->at("value")->stats.used != ctx.is_get_stats) {
+ return false;
+ }
+ if (result->val_str.parts.empty() || !result->val_str.parts.front().is_input ||
+ !result->val_str.parts.back().is_input) {
+ return false;
+ }
+ }
+ return true;
+ }));
+ }
+ for (auto & worker : workers) {
+ t.assert_true("request-local state", worker.get());
+ }
+ });
+
+}
+
static void test_whitespace_control(testing & t) {
test_template(t, "trim_blocks removes newline after tag",
"{% if true %}\n"
@@ -502,6 +545,13 @@ static void test_expressions(testing & t) {
"c"
);
+ test_template(t, "array index boundaries",
+ "{% for items in arrays %}{% for i in indices %}{{ items[i] | default('_') }}|{% endfor %};{% endfor %}",
+ {{"arrays", json::array({json::array({"a", "b", "c"}), json::array()})},
+ {"indices", json::array({INT64_MIN, -4, -3, -1, 0, 2, 3, INT64_MAX})}},
+ "_|_|a|c|a|c|_|_|;_|_|_|_|_|_|_|_|;"
+ );
+
test_template(t, "array bool access",
"{{ items[true] }}",
{{"items", json::array({"a", "b", "c"})}},
@@ -575,6 +625,14 @@ static void test_expressions(testing & t) {
"('c', 'b', 'a')"
);
+ test_template(t, "multipart string comparison and access",
+ "{% set left = a + 'bc' %}{% set right = 'ab' + c %}{{ left == right }}|"
+ "{{ left[0] }}{{ left[1] }}{{ left[2] }}|{{ right[1:3] }}|"
+ "{{ left[3] is undefined }}|{{ empty[0] is undefined }}",
+ {{"a", "a"}, {"c", "c"}, {"empty", ""}},
+ "True|abc|bc|True|True"
+ );
+
test_template(t, "string slice negative step",
"{{ 'abcdef'[::-2] }}",
json::object(),
@@ -633,6 +691,31 @@ static void test_expressions(testing & t) {
"{{ " + std::string(1000, '(') + "1" + std::string(1000, ')') + " }}",
"template is nested more than 128 levels deep"
);
+
+ test_template_error(t, "undefined hint: missing object key (attr)",
+ "{% set m = {'a': 1} %}{{ m.missing_key() }}",
+ "Callee is not a function: got Undefined (hint: 'missing_key')"
+ );
+
+ test_template_error(t, "undefined hint: missing object key (subscript)",
+ "{% set m = {'a': 1} %}{{ m['missing_key']() }}",
+ "Callee is not a function: got Undefined (hint: 'missing_key')"
+ );
+
+ test_template_error(t, "undefined hint: unknown array attribute",
+ "{% set a = [1] %}{{ a.missing_key() }}",
+ "Callee is not a function: got Undefined (hint: 'missing_key')"
+ );
+
+ test_template_error(t, "undefined hint: unknown string attribute",
+ "{% set s = 'x' %}{{ s.missing_key() }}",
+ "Callee is not a function: got Undefined (hint: 'missing_key')"
+ );
+
+ test_template_error(t, "undefined hint: unknown int attribute",
+ "{% set n = 1 %}{{ n.missing_key() }}",
+ "Callee is not a function: got Undefined (hint: 'missing_key')"
+ );
}
static void test_set_statement(testing & t) {
@@ -1440,6 +1523,17 @@ static void test_tests(testing & t) {
"yes"
);
+ test_template(t, "is divisibleby -1 with INT64_MIN",
+ "{{ x is divisibleby(-1) }}",
+ {{"x", INT64_MIN}},
+ "True"
+ );
+
+ test_template_error(t, "is divisibleby zero",
+ "{{ 10 is divisibleby(0) }}",
+ "divisibleby: division by zero"
+ );
+
test_template(t, "is eq",
"{{ 'yes' if 3 is eq(3) }}",
json::object(),
@@ -2730,6 +2824,27 @@ static void test_string_parts(testing & t) {
return runtime.gather_string_parts(runtime.execute(ast))->as_string();
};
+ t.test("comparison across part boundaries", [](testing & t) {
+ jinja::string left, right;
+ left.parts = {{true, "a"}, {false, "bc"}};
+ right.parts = {{false, "ab"}, {true, "c"}};
+ t.assert_true("equal text with different partitions", left.equals(right) && right.equals(left));
+
+ right.parts = {{false, ""}, {false, "ab"}, {true, ""}, {true, "c"}, {false, ""}};
+ t.assert_true("empty parts do not change equality", left.equals(right) && right.equals(left));
+ t.assert_true("different bytes are unequal", !left.equals(jinja::string("abd")));
+ t.assert_true("prefixes are unequal", !left.equals(jinja::string("ab")) && !jinja::string("ab").equals(left));
+
+ jinja::string empty;
+ empty.parts = {{true, ""}, {false, ""}};
+ t.assert_true("empty partitions equal an empty string", empty.equals(jinja::string()) && jinja::string().equals(empty));
+
+ auto object = jinja::mk_val<jinja::value_object>();
+ object->insert(jinja::mk_val<jinja::value_string>(left), jinja::mk_val<jinja::value_int>(7));
+ const auto found = object->find(jinja::mk_val<jinja::value_string>(right));
+ t.assert_true("object keys ignore partition boundaries", found && found->as_int() == 7);
+ });
+
t.test("merge joins only the neighbours with the same type", [](testing & t) {
// "AB" comes from the input and merges, "-" comes from the template and must not
jinja::string res = render("{{ val.a }}{{ val.b }}-{{ val.c }}",