Commit 82e1e2fd8 for llama.cpp

commit 82e1e2fd84b93c0dce5b312b333117f85dddc5b4
Author: Yuri Khrustalev <ykhrustalev@users.noreply.github.com>
Date:   Thu Oct 15 04:49:40 2026 -0400

    jinja : write nan and inf in tojson as json.dumps does (#30483)

    tojson wrote nan, inf and -inf for these floats, as values and as dict
    keys; json.dumps writes NaN, Infinity and -Infinity. The string and safe
    filters of a float keep nan and inf, as Python's str() does, so they no
    longer use the tojson function. float_to_json is renamed float_repr, as
    it now gives str(); float_json gives the json.dumps text.

    Also say in a comment why a last digit 9 is not bumped at a power of 2.

diff --git a/common/jinja/value.cpp b/common/jinja/value.cpp
index aed834ce4..0ecbbf8a8 100644
--- a/common/jinja/value.cpp
+++ b/common/jinja/value.cpp
@@ -244,6 +244,8 @@ static std::string json_ensure_ascii_preserving_format(const std::string & json_
     return output;
 }

+static std::string float_repr(double val);
+
 static value tojson(const func_args & args) {
     args.ensure_count(1, 5);
     value val_ascii      = args.get_kwarg_or_pos("ensure_ascii", 1);
@@ -634,6 +636,11 @@ const func_builtins & value_int_t::get_builtins() const {


 const func_builtins & value_float_t::get_builtins() const {
+    // Python's str() of a float, nan and inf stay as they are (tojson writes NaN and Infinity)
+    static const func_handler tostring = [](const func_args & args) -> value {
+        args.ensure_vals<value_float>();
+        return mk_val<value_string>(float_repr(args.get_pos(0)->as_float()));
+    };
     static const func_builtins builtins = {
         {"default", default_value},
         {"abs", [](const func_args & args) -> value {
@@ -650,8 +657,8 @@ const func_builtins & value_float_t::get_builtins() const {
             args.ensure_vals<value_float>();
             return mk_val<value_float>(args.get_pos(0)->as_float());
         }},
-        {"safe", tojson},
-        {"string", tojson},
+        {"safe", tostring},
+        {"string", tostring},
         {"tojson", tojson},
     };
     return builtins;
@@ -1522,10 +1529,10 @@ void global_from_json(context & ctx, const common_json & json_obj, bool mark_inp
     }
 }

-// a float as Python writes it (repr, json.dumps): the shortest digits that read back as the same value
-static std::string float_to_json(double val) {
+// a float as Python's repr() and str() write it: the shortest digits that read back as the same value
+static std::string float_repr(double val) {
     if (!std::isfinite(val)) {
-        return std::isnan(val) ? "nan" : val < 0 ? "-inf" : "inf"; // as before (json.dumps writes NaN and Infinity, str() writes nan and inf)
+        return std::isnan(val) ? "nan" : val < 0 ? "-inf" : "inf";
     }
     const double mag = std::fabs(val);

@@ -1551,6 +1558,7 @@ static std::string float_to_json(double val) {
         if (prec == 16 || reads_back(d)) {
             digits = d;
         } else if (is_pow2 && d.back() != '9') {
+            // a last digit 9 is not bumped (it would carry): no power of 2, from 2^-1074 to 2^1023, needs it
             d.back()++;
             if (reads_back(d)) {
                 digits = d;
@@ -1581,6 +1589,17 @@ static std::string float_to_json(double val) {
     return out;
 }

+// a float as json.dumps writes it: nan and inf as NaN and Infinity, which are not valid JSON
+static std::string float_json(double val) {
+    if (std::isnan(val)) {
+        return "NaN";
+    }
+    if (std::isinf(val)) {
+        return val < 0 ? "-Infinity" : "Infinity";
+    }
+    return float_repr(val);
+}
+
 // recursively convert value to JSON string
 // TODO: avoid circular references
 static void value_to_json_internal(std::ostringstream & oss, const value & val, int curr_lvl, int indent, const std::string_view item_sep, const std::string_view key_sep) {
@@ -1598,7 +1617,7 @@ static void value_to_json_internal(std::ostringstream & oss, const value & val,
     } else if (is_val<value_int>(val)) {
         oss << val->as_int();
     } else if (is_val<value_float>(val)) {
-        oss << float_to_json(val->as_float());
+        oss << float_json(val->as_float());
     } else if (is_val<value_string>(val)) {
         oss << "\"";
         for (char c : val->as_string().str()) {
@@ -1645,7 +1664,8 @@ static void value_to_json_internal(std::ostringstream & oss, const value & val,
             size_t i = 0;
             for (const auto & pair : obj) {
                 oss << indent_str() << (indent > 0 ? std::string(indent, ' ') : "");
-                value_to_json_internal(oss, mk_val<value_string>(pair.first->as_string().str()), curr_lvl + 1, indent, item_sep, key_sep);
+                const std::string key = is_val<value_float>(pair.first) ? float_json(pair.first->as_float()) : pair.first->as_string().str();
+                value_to_json_internal(oss, mk_val<value_string>(key), curr_lvl + 1, indent, item_sep, key_sep);
                 oss << key_sep;
                 value_to_json_internal(oss, pair.second, curr_lvl + 1, indent, item_sep, key_sep);
                 if (i < obj.size() - 1) {
diff --git a/tests/test-jinja.cpp b/tests/test-jinja.cpp
index 9b53e8cec..7132c9e22 100644
--- a/tests/test-jinja.cpp
+++ b/tests/test-jinja.cpp
@@ -1083,6 +1083,13 @@ static void test_literals(testing & t) {
         "1.0 1e+308 inf"
     );

+    // json.dumps writes NaN and Infinity, str() writes nan and inf
+    test_template(t, "nan and inf",
+        "{{ [p|float, n|float, q|float]|tojson }} {{ {p|float: 1}|tojson }} {{ p|float|string }} {{ n|float|string }} {{ q|float|safe }}",
+        {{"p", "inf"}, {"n", "-inf"}, {"q", "nan"}},
+        "[Infinity, -Infinity, NaN] {\"Infinity\": 1} inf -inf nan"
+    );
+
     test_template(t, "string|tojson",
         "{{ 'hello' | tojson }}",
         json::object(),