diff --git a/include/minja/minja.hpp b/include/minja/minja.hpp index 9ae377d..8160a72 100644 --- a/include/minja/minja.hpp +++ b/include/minja/minja.hpp @@ -1291,6 +1291,12 @@ class UnaryOpExpr : public Expression { } }; +static bool in(const Value & value, const Value & container) { + return (((container.is_array() || container.is_object()) && container.contains(value)) || + (value.is_string() && container.is_string() && + container.to_str().find(value.to_str()) != std::string::npos)); +}; + class BinaryOpExpr : public Expression { public: enum class Op { StrConcat, Add, Sub, Mul, MulMul, Div, DivDiv, Mod, Eq, Ne, Lt, Gt, Le, Ge, And, Or, In, NotIn, Is, IsNot }; @@ -1355,13 +1361,8 @@ class BinaryOpExpr : public Expression { case Op::Gt: return l > r; case Op::Le: return l <= r; case Op::Ge: return l >= r; - case Op::In: return (((r.is_array() || r.is_object()) && r.contains(l)) || - (l.is_string() && r.is_string() && - r.to_str().find(l.to_str()) != std::string::npos)); - case Op::NotIn: - return !(((r.is_array() || r.is_object()) && r.contains(l)) || - (l.is_string() && r.is_string() && - r.to_str().find(l.to_str()) != std::string::npos)); + case Op::In: return in(l, r); + case Op::NotIn: return !in(l, r); default: break; } throw std::runtime_error("Unknown binary operator"); @@ -1500,6 +1501,13 @@ class MethodCallExpr : public Expression { } else if (method->get_name() == "pop") { vargs.expectArgs("pop method", {1, 1}, {0, 0}); return obj.pop(vargs.args[0]); + } else if (method->get_name() == "keys") { + vargs.expectArgs("keys method", {0, 0}, {0, 0}); + auto result = Value::array(); + for (const auto& key : obj.keys()) { + result.push_back(Value(key)); + } + return result; } else if (method->get_name() == "get") { vargs.expectArgs("get method", {1, 2}, {0, 0}); auto key = vargs.args[0]; @@ -2792,6 +2800,9 @@ inline std::shared_ptr Context::builtins() { if (!items.is_array()) throw std::runtime_error("object is not iterable"); return items; })); + globals.set("in", simple_function("in", { "item", "items" }, [](const std::shared_ptr &, Value & args) -> Value { + return in(args.at("item"), args.at("items")); + })); globals.set("unique", simple_function("unique", { "items" }, [](const std::shared_ptr &, Value & args) -> Value { auto & items = args.at("items"); if (!items.is_array()) throw std::runtime_error("object is not iterable"); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 14e789b..3dca28b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -320,6 +320,9 @@ set(MODEL_IDS xwen-team/Xwen-72B-Chat xwen-team/Xwen-7B-Chat Qwen/Qwen3-4B + Qwen/Qwen3-235B-A22B-Instruct-2507 + Qwen/Qwen3-235B-A22B-Thinking-2507 + Qwen/Qwen3-Coder-30B-A3B-Instruct # Broken, TODO: # Qwen/QwQ-32B diff --git a/tests/test-syntax.cpp b/tests/test-syntax.cpp index 55e9b0b..dfef761 100644 --- a/tests/test-syntax.cpp +++ b/tests/test-syntax.cpp @@ -217,6 +217,10 @@ TEST(SyntaxTest, SimpleCases) { render(R"({{ 'a' in 'abc' }},{{ 'd' in 'abc' }})", {}, {})); EXPECT_EQ("False,True", render(R"({{ 'a' not in 'abc' }},{{ 'd' not in 'abc' }})", {}, {})); + EXPECT_EQ("['a', 'a']", + render(R"({{ ['a', 'b', 'c', 'a'] | select('in', ['a']) | list }})", {}, {})); + EXPECT_EQ("['a', 'b'],[]", + render(R"({{ {'a': 1, 'b': 2}.keys() | list }},{{ {}.keys() | list }})", {}, {})); EXPECT_EQ( R"([{'a': 1}])", render(R"({{ [{"a": 1}, {"a": 2}, {}] | selectattr("a", "equalto", 1) | list }})", {}, {}));