// not thread-safe
void enable_debug(bool enable);
+// for visiting AST nodes
+// function signature: void(bool is_leaf, statement * node, pair of <label, children>)
+using visitor_pair = std::pair<std::string, std::vector<statement *>>;
+using visitor_fn = std::function<void(bool, statement *, std::vector<visitor_pair>)>;
+
struct context {
std::shared_ptr<std::string> src; // for debugging; use shared_ptr to avoid copying on scope creation
std::time_t current_time; // for functions that need current time
bool is_get_stats = false; // whether to collect stats
+ visitor_fn visitor;
+
// src is optional, used for error reporting
context(std::string src = "") : src(std::make_shared<std::string>(std::move(src))) {
env = mk_val<value_object>();
value_object env;
};
+// utils for visiting AST nodes
+static std::vector<statement *> stmts_to_ptr(const statements & stmts) {
+ std::vector<statement *> children;
+ for (const auto & stmt : stmts) {
+ children.push_back(stmt.get());
+ }
+ return children;
+}
+
/**
* Base class for all nodes in the AST.
*/
size_t pos; // position in source, for debugging
virtual ~statement() = default;
virtual std::string type() const { return "Statement"; }
+ virtual void visit(context & ctx) { ctx.visitor(true, this, {}); }
// execute_impl must be overridden by derived classes
virtual value execute_impl(context &) { throw_exec_error(); }
std::string type() const override { return "If"; }
value execute_impl(context & ctx) override;
+ void visit(context & ctx) override {
+ ctx.visitor(false, this, {
+ {"test", {test.get()}},
+ {"body", stmts_to_ptr(body)},
+ {"alternate", stmts_to_ptr(alternate)}
+ });
+ }
};
struct identifier;
std::string type() const override { return "For"; }
value execute_impl(context & ctx) override;
+ void visit(context & ctx) override {
+ ctx.visitor(false, this, {
+ {"loopvar", {loopvar.get()}},
+ {"iterable", {iterable.get()}},
+ {"body", stmts_to_ptr(body)},
+ {"default_block", stmts_to_ptr(default_block)}
+ });
+ }
};
struct break_statement : public statement {
std::string type() const override { return "Set"; }
value execute_impl(context & ctx) override;
+ void visit(context & ctx) override {
+ ctx.visitor(false, this, {
+ {"assignee", {assignee.get()}},
+ {"value", {val.get()}},
+ {"body", stmts_to_ptr(body)}
+ });
+ }
};
struct macro_statement : public statement {
std::string type() const override { return "Macro"; }
value execute_impl(context & ctx) override;
+ void visit(context & ctx) override {
+ ctx.visitor(false, this, {
+ {"name", {name.get()}},
+ {"args", stmts_to_ptr(args)},
+ {"body", stmts_to_ptr(body)}
+ });
+ }
};
struct comment_statement : public statement {
}
std::string type() const override { return "MemberExpression"; }
value execute_impl(context & ctx) override;
+ void visit(context & ctx) override {
+ ctx.visitor(false, this, {
+ {"object", {object.get()}},
+ {"property", {property.get()}}
+ });
+ }
};
struct call_expression : public expression {
}
std::string type() const override { return "CallExpression"; }
value execute_impl(context & ctx) override;
+ void visit(context & ctx) override {
+ ctx.visitor(false, this, {
+ {"callee", {callee.get()}},
+ {"args", stmts_to_ptr(args)}
+ });
+ }
};
/**
}
std::string type() const override { return "BinaryExpression"; }
value execute_impl(context & ctx) override;
+ void visit(context & ctx) override {
+ ctx.visitor(false, this, {
+ {"left", {left.get()}},
+ {"right", {right.get()}}
+ });
+ }
};
/**
std::string type() const override { return "FilterExpression"; }
value execute_impl(context & ctx) override;
+ void visit(context & ctx) override {
+ ctx.visitor(false, this, {
+ {"operand", {operand.get()}},
+ {"filter", {filter.get()}}
+ });
+ }
};
struct filter_statement : public statement {
}
std::string type() const override { return "FilterStatement"; }
value execute_impl(context & ctx) override;
+ void visit(context & ctx) override {
+ ctx.visitor(false, this, {
+ {"filter", {filter.get()}},
+ {"body", stmts_to_ptr(body)}
+ });
+ }
};
/**
}
return lhs->execute_impl(ctx);
}
+ void visit(context & ctx) override {
+ ctx.visitor(false, this, {
+ {"lhs", {lhs.get()}},
+ {"test", {test.get()}}
+ });
+ }
};
/**
}
std::string type() const override { return "TestExpression"; }
value execute_impl(context & ctx) override;
+ void visit(context & ctx) override {
+ ctx.visitor(false, this, {
+ {"operand", {operand.get()}},
+ {"test", {test.get()}}
+ });
+ }
};
/**
}
std::string type() const override { return "UnaryExpression"; }
value execute_impl(context & ctx) override;
+ void visit(context & ctx) override {
+ ctx.visitor(false, this, {
+ {"argument", {argument.get()}}
+ });
+ }
};
struct slice_expression : public expression {
[[noreturn]] value execute_impl(context &) override {
throw std::runtime_error("must be handled by MemberExpression");
}
+ void visit(context & ctx) override {
+ ctx.visitor(false, this, {
+ {"start_expr", {start_expr.get()}},
+ {"stop_expr", {stop_expr.get()}},
+ {"step_expr", {step_expr.get()}}
+ });
+ }
};
struct keyword_argument_expression : public expression {
}
std::string type() const override { return "KeywordArgumentExpression"; }
value execute_impl(context & ctx) override;
+ void visit(context & ctx) override {
+ ctx.visitor(false, this, {
+ {"key", {key.get()}},
+ {"val", {val.get()}}
+ });
+ }
};
struct spread_expression : public expression {
chk_type<expression>(this->argument);
}
std::string type() const override { return "SpreadExpression"; }
+ void visit(context & ctx) override {
+ ctx.visitor(false, this, {
+ {"argument", {argument.get()}}
+ });
+ }
};
struct call_statement : public statement {
}
std::string type() const override { return "CallStatement"; }
value execute_impl(context & ctx) override;
+ void visit(context & ctx) override {
+ ctx.visitor(false, this, {
+ {"call", {call.get()}},
+ {"caller_args", stmts_to_ptr(caller_args)},
+ {"body", stmts_to_ptr(body)}
+ });
+ }
};
struct ternary_expression : public expression {
return false_expr->execute(ctx);
}
}
+ void visit(context & ctx) override {
+ ctx.visitor(false, this, {
+ {"condition", {condition.get()}},
+ {"true_expr", {true_expr.get()}},
+ {"false_expr", {false_expr.get()}}
+ });
+ }
};
struct raised_exception : public std::exception {
}
return parts;
}
+
+ static std::string debug_dump_program(const program & prog, const std::string & src);
};
} // namespace jinja
static int main_automated_tests(void);
static void run_multiple(const std::string& dir_path, bool stop_on_first_failure, const json& input, bool use_common = false);
-static void run_single(const std::string& contents, json input, bool use_common = false, const std::string & output_path = "");
+static void run_single(const std::string& contents, json input, bool use_common = false, bool dump_prog = false, const std::string & output_path = "");
static std::string HELP = R"(
Usage: test-chat-template [OPTIONS] PATH_TO_TEMPLATE
--json <path> Path to the JSON input file.
--stop-on-first-fail Stop testing on the first failure (default: false).
--no-common Use direct Jinja engine instead of common chat templates (default: use common).
+ --dump-prog Dump the parsed program for debugging (only for single template runs).
--output <path> Path to output results (only for single template runs).
If PATH_TO_TEMPLATE is a file, runs that single template.
If PATH_TO_TEMPLATE is a directory, runs all .jinja files in that directory.
std::string & json_to_use = DEFAULT_JSON;
bool stop_on_first_fail = false;
bool use_common = true;
+ bool dump_prog = false;
for (size_t i = 1; i < args.size(); i++) {
if (args[i] == "--help" || args[i] == "-h") {
i++;
} else if (args[i] == "--no-common") {
use_common = false;
+ } else if (args[i] == "--dump-prog") {
+ dump_prog = true;
} else if (tmpl_path.empty()) {
tmpl_path = args[i];
} else {
std::string contents = std::string(
std::istreambuf_iterator<char>(infile),
std::istreambuf_iterator<char>());
- run_single(contents, input_json, use_common, output_path);
+ run_single(contents, input_json, use_common, dump_prog, output_path);
} else {
std::cerr << "Error: PATH_TO_TEMPLATE is not a valid file or directory: " << tmpl_path << "\n";
return 1;
}
-void run_single(const std::string& contents, json input, bool use_common, const std::string & output_path) {
+void run_single(const std::string& contents, json input, bool use_common, bool dump_prog, const std::string & output_path) {
jinja::enable_debug(true);
jinja::value_string output_parts;
+ if (dump_prog) {
+ jinja::lexer lexer;
+ auto lexer_res = lexer.tokenize(contents);
+ jinja::program ast = jinja::parse_from_tokens(lexer_res);
+ std::string prog_dump = jinja::runtime::debug_dump_program(ast, contents);
+ std::cout << "\n=== DUMPED PROGRAM ===\n";
+ std::cout << prog_dump << "\n";
+ return;
+ }
+
if (use_common) {
std::string bos_token = "<s>";
std::string eos_token = "</s>";