diff --git a/.gitignore b/.gitignore index a6dc78d..2e10a38 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /release/ /.vscode/ /vcpkg_installed/ +/_codeql_build_dir/ CMakeUserPresets.json node_modules diff --git a/generated/include/grammar/syntax.h b/generated/include/grammar/syntax.h index 1d050e6..d96909b 100644 --- a/generated/include/grammar/syntax.h +++ b/generated/include/grammar/syntax.h @@ -29,20 +29,18 @@ constexpr alioth::SymbolID DOT = 11; constexpr alioth::SymbolID ELLIPSES = 12; constexpr alioth::SymbolID EMPTY = 13; constexpr alioth::SymbolID LANG = 14; -constexpr alioth::SymbolID USING = 15; -constexpr alioth::SymbolID STRING = 16; -constexpr alioth::SymbolID ID = 17; -constexpr alioth::SymbolID REGEX = 18; -constexpr alioth::SymbolID COMMENT = 19; -constexpr alioth::SymbolID SPACE = 20; +constexpr alioth::SymbolID STRING = 15; +constexpr alioth::SymbolID ID = 16; +constexpr alioth::SymbolID REGEX = 17; +constexpr alioth::SymbolID COMMENT = 18; +constexpr alioth::SymbolID SPACE = 19; -struct EmptyFormula; // SymbolID = 33; Accepts: [33] -struct Formula; // SymbolID = 32; Accepts: [32, 34] -struct Grammar; // SymbolID = 22; Accepts: [22] -struct Ntrm; // SymbolID = 28; Accepts: [28] -struct Symbol; // SymbolID = 35; Accepts: [35] -struct Term; // SymbolID = 27; Accepts: [27] -struct Using; // SymbolID = 26; Accepts: [26] +struct EmptyFormula; // SymbolID = 30 +struct Formula; // SymbolID = 29 +struct Grammar; // SymbolID = 21 +struct Ntrm; // SymbolID = 25 +struct Symbol; // SymbolID = 32 +struct Term; // SymbolID = 24 @@ -61,7 +59,6 @@ struct Grammar : public alioth::ASTView { alioth::AST lang() const; std::vector ntrms() const; std::vector terms() const; - std::vector usings() const; using ASTView::ASTView; }; @@ -69,7 +66,6 @@ struct Ntrm : public alioth::ASTView { struct Define; struct External; struct Formed; - struct Using; alioth::AST name() const; using ASTView::ASTView; @@ -93,12 +89,6 @@ struct Ntrm::Formed: public Ntrm { std::vector formulas() const; alioth::AST name() const; }; -struct Ntrm::Using: public Ntrm { - using Ntrm::Ntrm; - - alioth::AST lang() const; - alioth::AST name() const; -}; struct Symbol : public alioth::ASTView { @@ -109,30 +99,10 @@ struct Symbol : public alioth::ASTView { }; struct Term : public alioth::ASTView { - struct Define; - struct Using; - - alioth::AST name() const; - using ASTView::ASTView; -}; -struct Term::Define: public Term { - using Term::Term; - std::vector contexts() const; alioth::AST name() const; alioth::AST optional() const; alioth::AST regex() const; -}; -struct Term::Using: public Term { - using Term::Term; - - alioth::AST lang() const; - alioth::AST name() const; -}; - - -struct Using : public alioth::ASTView { - alioth::AST grammar() const; using ASTView::ASTView; }; @@ -143,7 +113,7 @@ namespace alioth { template<> inline grammar::EmptyFormula alioth::ASTNode::As() { switch(id) { - case 33: + case 30: return grammar::EmptyFormula{shared_from_this()}; default: return {}; @@ -153,7 +123,7 @@ inline grammar::EmptyFormula alioth::ASTNode::As() { template<> inline grammar::Formula alioth::ASTNode::As() { switch(id) { - case 32:case 34: + case 29: case 31: return grammar::Formula{shared_from_this()}; default: return {}; @@ -163,7 +133,7 @@ inline grammar::Formula alioth::ASTNode::As() { template<> inline grammar::Grammar alioth::ASTNode::As() { switch(id) { - case 22: + case 21: return grammar::Grammar{shared_from_this()}; default: return {}; @@ -173,7 +143,7 @@ inline grammar::Grammar alioth::ASTNode::As() { template<> inline grammar::Ntrm alioth::ASTNode::As() { switch(id) { - case 28: + case 25: return grammar::Ntrm{shared_from_this()}; default: return {}; @@ -182,7 +152,7 @@ inline grammar::Ntrm alioth::ASTNode::As() { template<> inline grammar::Ntrm::Define alioth::ASTNode::As() { switch(OriginFormula()) { - case 24: + case 16: return grammar::Ntrm::Define{shared_from_this()}; default: return {}; @@ -191,7 +161,7 @@ inline grammar::Ntrm::Define alioth::ASTNode::As() { template<> inline grammar::Ntrm::External alioth::ASTNode::As() { switch(OriginFormula()) { - case 26:case 27: + case 18: return grammar::Ntrm::External{shared_from_this()}; default: return {}; @@ -200,27 +170,18 @@ inline grammar::Ntrm::External alioth::ASTNode::As() { template<> inline grammar::Ntrm::Formed alioth::ASTNode::As() { switch(OriginFormula()) { - case 25: + case 17: return grammar::Ntrm::Formed{shared_from_this()}; default: return {}; } } -template<> -inline grammar::Ntrm::Using alioth::ASTNode::As() { - switch(OriginFormula()) { - case 28: - return grammar::Ntrm::Using{shared_from_this()}; - default: - return {}; - } -} template<> inline grammar::Symbol alioth::ASTNode::As() { switch(id) { - case 35: + case 32: return grammar::Symbol{shared_from_this()}; default: return {}; @@ -230,41 +191,12 @@ inline grammar::Symbol alioth::ASTNode::As() { template<> inline grammar::Term alioth::ASTNode::As() { switch(id) { - case 27: + case 24: return grammar::Term{shared_from_this()}; default: return {}; } } -template<> -inline grammar::Term::Define alioth::ASTNode::As() { - switch(OriginFormula()) { - case 16:case 17:case 18:case 19: - return grammar::Term::Define{shared_from_this()}; - default: - return {}; - } -} -template<> -inline grammar::Term::Using alioth::ASTNode::As() { - switch(OriginFormula()) { - case 20: - return grammar::Term::Using{shared_from_this()}; - default: - return {}; - } -} - - -template<> -inline grammar::Using alioth::ASTNode::As() { - switch(id) { - case 26: - return grammar::Using{shared_from_this()}; - default: - return {}; - } -} template<> @@ -273,87 +205,83 @@ inline Syntax SyntaxOf() { using namespace nlohmann; auto lex = Lexicon::Builder("grammar"); lex.Define("ARROW", R"(->)"_regex); - lex.Define("LT", R"(<)"_regex); - lex.Define("GT", R"(>)"_regex); - lex.Define("OR", R"(\|)"_regex); - lex.Define("ASSIGN", R"(=)"_regex); - lex.Define("OPTIONAL", R"(\?)"_regex); - lex.Define("AT", R"(@)"_regex); - lex.Define("SEMICOLON", R"(;)"_regex); - lex.Define("COLON", R"(:)"_regex); - lex.Define("COMMA", R"(,)"_regex); - lex.Define("DOT", R"(\.)"_regex); - lex.Define("ELLIPSES", R"(\.\.\.)"_regex); - lex.Define("EMPTY", R"(%empty)"_regex); - lex.Define("LANG", R"(lang)"_regex, { "keyword", }); - - lex.Define("USING", R"(using)"_regex, { "keyword", }); - lex.Define("STRING", R"(\"([^\"\n\\]|\\[^\n])*\")"_regex, { "json", }); - lex.Define("ID", R"([a-zA-Z_]\w*)"_regex); - lex.Define("REGEX", R"(\/([^\\\/]|\\[^\n])+\/)"_regex); - lex.Define("COMMENT", R"(#[^\n]*\n)"_regex); - lex.Define("SPACE", R"(\s+)"_regex); - auto syntax = Syntactic::Builder(lex.Build()); syntax.Ignore("SEMICOLON"); syntax.Ignore("COMMENT"); syntax.Ignore("SPACE"); + // grammar -> LANG COLON ID@lang ...terms? ...ntrms?; syntax.Formula("grammar").Symbol("LANG").Symbol("COLON").Symbol("ID", "lang").Commit(); - syntax.Formula("grammar").Symbol("LANG").Symbol("COLON").Symbol("ID", "lang").Symbol("usings", "...").Commit(); syntax.Formula("grammar").Symbol("LANG").Symbol("COLON").Symbol("ID", "lang").Symbol("terms", "...").Commit(); - syntax.Formula("grammar").Symbol("LANG").Symbol("COLON").Symbol("ID", "lang").Symbol("usings", "...").Symbol("terms", "...").Commit(); syntax.Formula("grammar").Symbol("LANG").Symbol("COLON").Symbol("ID", "lang").Symbol("ntrms", "...").Commit(); - syntax.Formula("grammar").Symbol("LANG").Symbol("COLON").Symbol("ID", "lang").Symbol("usings", "...").Symbol("ntrms", "...").Commit(); syntax.Formula("grammar").Symbol("LANG").Symbol("COLON").Symbol("ID", "lang").Symbol("terms", "...").Symbol("ntrms", "...").Commit(); - syntax.Formula("grammar").Symbol("LANG").Symbol("COLON").Symbol("ID", "lang").Symbol("usings", "...").Symbol("terms", "...").Symbol("ntrms", "...").Commit(); - syntax.Formula("usings").Symbol("using", "usings").Commit(); - syntax.Formula("usings").Symbol("usings", "...").Symbol("using", "usings").Commit(); + + // terms -> ...terms? term@terms; syntax.Formula("terms").Symbol("term", "terms").Commit(); syntax.Formula("terms").Symbol("terms", "...").Symbol("term", "terms").Commit(); + + // ntrms -> ...ntrms? ntrm@ntrms; syntax.Formula("ntrms").Symbol("ntrm", "ntrms").Commit(); syntax.Formula("ntrms").Symbol("ntrms", "...").Symbol("ntrm", "ntrms").Commit(); - syntax.Formula("using").Symbol("USING").Symbol("STRING", "grammar").Commit(); - syntax.Formula("term", "define").Symbol("ID", "name").Symbol("ASSIGN").Symbol("REGEX", "regex").Commit(); - syntax.Formula("term", "define").Symbol("ID", "name").Symbol("contexts", "...").Symbol("ASSIGN").Symbol("REGEX", "regex").Commit(); - syntax.Formula("term", "define").Symbol("ID", "name").Symbol("OPTIONAL", "optional").Symbol("ASSIGN").Symbol("REGEX", "regex").Commit(); - syntax.Formula("term", "define").Symbol("ID", "name").Symbol("contexts", "...").Symbol("OPTIONAL", "optional").Symbol("ASSIGN").Symbol("REGEX", "regex").Commit(); - syntax.Formula("term", "using").Symbol("ELLIPSES").Symbol("ASSIGN").Symbol("ID", "lang").Symbol("DOT").Symbol("ID", "name").Commit(); + + // term -> ID@name ...contexts? OPTIONAL?@optional ASSIGN REGEX@regex; + syntax.Formula("term").Symbol("ID", "name").Symbol("ASSIGN").Symbol("REGEX", "regex").Commit(); + syntax.Formula("term").Symbol("ID", "name").Symbol("contexts", "...").Symbol("ASSIGN").Symbol("REGEX", "regex").Commit(); + syntax.Formula("term").Symbol("ID", "name").Symbol("OPTIONAL", "optional").Symbol("ASSIGN").Symbol("REGEX", "regex").Commit(); + syntax.Formula("term").Symbol("ID", "name").Symbol("contexts", "...").Symbol("OPTIONAL", "optional").Symbol("ASSIGN").Symbol("REGEX", "regex").Commit(); + + // contexts -> LT ...context_list GT; syntax.Formula("contexts").Symbol("LT").Symbol("context_list", "...").Symbol("GT").Commit(); + + // context_list -> ID@contexts | ...context_list COMMA ID@contexts; syntax.Formula("context_list").Symbol("ID", "contexts").Commit(); syntax.Formula("context_list").Symbol("context_list", "...").Symbol("COMMA").Symbol("ID", "contexts").Commit(); + + // ntrm.define -> ID@name ARROW ...formula_group SEMICOLON; syntax.Formula("ntrm", "define").Symbol("ID", "name").Symbol("ARROW").Symbol("formula_group", "...").Symbol("SEMICOLON").Commit(); + + // ntrm.formed -> ID@name DOT ID@form ARROW ...formula_group SEMICOLON; syntax.Formula("ntrm", "formed").Symbol("ID", "name").Symbol("DOT").Symbol("ID", "form").Symbol("ARROW").Symbol("formula_group", "...").Symbol("SEMICOLON").Commit(); + + // ntrm.external -> ID@name ARROW STRING@grammar SEMICOLON; syntax.Formula("ntrm", "external").Symbol("ID", "name").Symbol("ARROW").Symbol("STRING", "grammar").Symbol("SEMICOLON").Commit(); - syntax.Formula("ntrm", "external").Symbol("ELLIPSES").Symbol("ARROW").Symbol("STRING", "grammar").Symbol("SEMICOLON").Commit(); - syntax.Formula("ntrm", "using").Symbol("ELLIPSES").Symbol("ARROW").Symbol("ID", "lang").Symbol("DOT").Symbol("ID", "name").Symbol("SEMICOLON").Commit(); + + // formula_group -> formula@formulas | empty_formula@formulas | ...formula_group OR formula@formulas | ...formula_group OR empty_formula@formulas; syntax.Formula("formula_group").Symbol("formula", "formulas").Commit(); syntax.Formula("formula_group").Symbol("empty_formula", "formulas").Commit(); syntax.Formula("formula_group").Symbol("formula_group", "...").Symbol("OR").Symbol("formula", "formulas").Commit(); syntax.Formula("formula_group").Symbol("formula_group", "...").Symbol("OR").Symbol("empty_formula", "formulas").Commit(); + + // formula -> ...formula_body; syntax.Formula("formula").Symbol("formula_body", "...").Commit(); + + // formula_body -> ...formula_body? symbol@symbols; syntax.Formula("formula_body").Symbol("symbol", "symbols").Commit(); syntax.Formula("formula_body").Symbol("formula_body", "...").Symbol("symbol", "symbols").Commit(); + + // empty_formula -> EMPTY@empty; syntax.Formula("empty_formula").Symbol("EMPTY", "empty").Commit(); + + // symbol -> ID@name OPTIONAL?@optional | ID@name OPTIONAL?@optional AT ID@attr | ELLIPSES@attr ID@name OPTIONAL?@optional; syntax.Formula("symbol").Symbol("ID", "name").Commit(); syntax.Formula("symbol").Symbol("ID", "name").Symbol("OPTIONAL", "optional").Commit(); syntax.Formula("symbol").Symbol("ID", "name").Symbol("AT").Symbol("ID", "attr").Commit(); @@ -400,14 +328,6 @@ inline std::vector Grammar::terms() const { } ); } -inline std::vector Grammar::usings() const { - return alioth::generic::collect( - (*this)->Attrs("usings"), - [](auto n) { - return n->template As(); - } - ); -} inline std::vector Ntrm::Define::formulas() const { return (*this)->Attrs("formulas"); } @@ -429,12 +349,6 @@ inline std::vector Ntrm::Formed::formulas() const { inline alioth::AST Ntrm::Formed::name() const { return (*this)->Attr("name"); } -inline alioth::AST Ntrm::Using::lang() const { - return (*this)->Attr("lang"); -} -inline alioth::AST Ntrm::Using::name() const { - return (*this)->Attr("name"); -} inline alioth::AST Ntrm::name() const { return (*this)->Attr("name"); @@ -448,33 +362,20 @@ inline alioth::AST Symbol::name() const { inline alioth::AST Symbol::optional() const { return (*this)->Attr("optional"); } -inline std::vector Term::Define::contexts() const { +inline std::vector Term::contexts() const { return (*this)->Attrs("contexts"); } -inline alioth::AST Term::Define::name() const { +inline alioth::AST Term::name() const { return (*this)->Attr("name"); } -inline alioth::AST Term::Define::optional() const { +inline alioth::AST Term::optional() const { return (*this)->Attr("optional"); } -inline alioth::AST Term::Define::regex() const { +inline alioth::AST Term::regex() const { return (*this)->Attr("regex"); } -inline alioth::AST Term::Using::lang() const { - return (*this)->Attr("lang"); -} -inline alioth::AST Term::Using::name() const { - return (*this)->Attr("name"); -} - -inline alioth::AST Term::name() const { - return (*this)->Attr("name"); -} -inline alioth::AST Using::grammar() const { - return (*this)->Attr("grammar"); -} } -#endif \ No newline at end of file +#endif diff --git a/grammar/grammar.grammar b/grammar/grammar.grammar index c14867a..54e57c6 100644 --- a/grammar/grammar.grammar +++ b/grammar/grammar.grammar @@ -17,31 +17,24 @@ DOT = /\./ ELLIPSES = /\.\.\./ EMPTY = /%empty/ LANG = /lang/ -USING = /using/ STRING = /\"([^\"\n\\]|\\[^\n])*\"/ ID = /[a-zA-Z_]\w*/ REGEX = /\/([^\\\/]|\\[^\n])+\// COMMENT ?= /#[^\n]*\n/ SPACE ?= /\s+/ -grammar -> LANG COLON ID@lang ...usings? ...terms? ...ntrms?; -usings -> ...usings? using@usings; +grammar -> LANG COLON ID@lang ...terms? ...ntrms?; terms -> ...terms? term@terms; ntrms -> ...ntrms? ntrm@ntrms; -using -> USING STRING@grammar; - -term.define -> ID@name ...contexts? OPTIONAL?@optional ASSIGN REGEX@regex; -term.using -> ELLIPSES ASSIGN ID@lang DOT ID@name; +term -> ID@name ...contexts? OPTIONAL?@optional ASSIGN REGEX@regex; contexts -> LT ...context_list GT; context_list -> ID@contexts | ...context_list COMMA ID@contexts; -ntrm.define -> ID@name ARROW ...formula_group SEMICOLON; +ntrm.define -> ID@name ARROW ...formula_group SEMICOLON; ntrm.formed -> ID@name DOT ID@form ARROW ...formula_group SEMICOLON; ntrm.external -> ID@name ARROW STRING@grammar SEMICOLON; -ntrm.external -> ELLIPSES ARROW STRING@grammar SEMICOLON; -ntrm.using -> ELLIPSES ARROW ID@lang DOT ID@name SEMICOLON; formula_group -> formula@formulas | empty_formula@formulas diff --git a/include/alioth/println_compat.h b/include/alioth/println_compat.h new file mode 100644 index 0000000..61f6052 --- /dev/null +++ b/include/alioth/println_compat.h @@ -0,0 +1,25 @@ +#ifndef __ALIOTH_PRINTLN_COMPAT_H__ +#define __ALIOTH_PRINTLN_COMPAT_H__ + +#include +#include + +namespace fmt { +// fmt::println was added in fmt 10.0.0 (FMT_VERSION >= 100000) +// This provides a compatibility implementation for older versions +#if FMT_VERSION < 100000 +template +inline void println(FILE* f, const S& format, Args&&... args) { + print(f, fmt::runtime(format), std::forward(args)...); + std::fputc('\n', f); +} + +template +inline void println(const S& format, Args&&... args) { + print(fmt::runtime(format), std::forward(args)...); + std::putchar('\n'); +} +#endif +} + +#endif diff --git a/include/aliox/grammar.h b/include/aliox/grammar.h index 54d3d52..24d9012 100644 --- a/include/aliox/grammar.h +++ b/include/aliox/grammar.h @@ -1,6 +1,8 @@ #ifndef __ALIOX_GRAMMAR_H__ #define __ALIOX_GRAMMAR_H__ +#include +#include #include #include "alioth/ast.h" @@ -8,6 +10,15 @@ namespace alioth { +/** + * 外部语法加载器类型 + * + * 用于加载外部符号引用的语法规则 + * 参数为外部语法的路径或标识符 + * 返回对应的语法规则,如果无法加载则返回nullptr + */ +using ExternalLoader = std::function; + struct Grammar { /** * 将文法定义编译为语法规则 @@ -15,6 +26,14 @@ struct Grammar { * @param grammar 文法源码 */ static Syntax Compile(Doc grammar); + + /** + * 将文法定义编译为语法规则 + * + * @param grammar 文法源码 + * @param loader 外部语法加载器,用于加载外部符号引用的语法 + */ + static Syntax Compile(Doc grammar, ExternalLoader loader); }; } // namespace alioth diff --git a/src/alioth-cli/parse.cpp b/src/alioth-cli/parse.cpp index c5b1cb0..d5b6e1b 100644 --- a/src/alioth-cli/parse.cpp +++ b/src/alioth-cli/parse.cpp @@ -3,6 +3,7 @@ #include "alioth-cli/syntax.h" #include "alioth/document.h" #include "alioth/parser.h" +#include "alioth/println_compat.h" #include "aliox/grammar.h" #include "aliox/skeleton.h" diff --git a/src/alioth-cli/skeleton.cpp b/src/alioth-cli/skeleton.cpp index a94c75d..e017c35 100644 --- a/src/alioth-cli/skeleton.cpp +++ b/src/alioth-cli/skeleton.cpp @@ -4,6 +4,7 @@ #include "alioth/alioth.h" #include "alioth/document.h" #include "alioth/parser.h" +#include "alioth/println_compat.h" #include "alioth/strings.h" #include "aliox/grammar.h" #include "aliox/skeleton.h" diff --git a/src/alioth/parser.cpp b/src/alioth/parser.cpp index f7752ac..ef6a9b4 100644 --- a/src/alioth/parser.cpp +++ b/src/alioth/parser.cpp @@ -1,5 +1,7 @@ #include "alioth/parser.h" +#include "alioth/println_compat.h" + namespace alioth { Parser::Parser(Syntax syntax, Doc doc) : Parser(syntax, doc, {}) {} diff --git a/src/alioth/syntax.cpp b/src/alioth/syntax.cpp index a27d816..cab3a62 100644 --- a/src/alioth/syntax.cpp +++ b/src/alioth/syntax.cpp @@ -2,6 +2,8 @@ #include +#include "alioth/println_compat.h" + namespace alioth { bool Syntactic::IsTerm(SymbolID id) const { diff --git a/src/aliox/grammar.cpp b/src/aliox/grammar.cpp index 6f2a016..37f883b 100644 --- a/src/aliox/grammar.cpp +++ b/src/aliox/grammar.cpp @@ -40,6 +40,10 @@ std::optional GetForm(grammar::Ntrm const& ntrm) { } // namespace Syntax Grammar::Compile(Doc grammar) { + return Compile(grammar, {}); +} + +Syntax Grammar::Compile(Doc grammar, ExternalLoader loader) { auto root = Parser(SyntaxOf(), grammar).Parse(); auto g = ViewOf(root); @@ -49,48 +53,36 @@ Syntax Grammar::Compile(Doc grammar) { // 处理终结符定义 for (auto const& term : g.terms()) { - // 跳过 term.using,它是复用其他语言的终结符 - // 在简化版中,我们不支持这个功能 - auto use = term->As(); - if (use) { - continue; - } - - auto define = term->As(); - if (!define) continue; - - auto src = define.regex()->Text(); + auto src = term.regex()->Text(); auto regex = RegexTree::Compile(src.substr(1, src.size() - 2)); - lex.Define(define.name()->Text(), regex, collect(define.contexts(), text())); + lex.Define(term.name()->Text(), regex, collect(term.contexts(), text())); } auto syntax = Syntactic::Builder(lex.Build()); // 标记可忽略的终结符 for (auto const& term : g.terms()) { - auto define = term->As(); - if (!define) continue; - - if (define.optional()) { - syntax.Ignore(define.name()->Text()); + if (term.optional()) { + syntax.Ignore(term.name()->Text()); } } // 处理非终结符定义 for (auto const& ntrm : g.ntrms()) { - // 跳过 ntrm.external,它是引用外部文法 - // 保留对外部符号机制的支持,但在这个简化版中只是跳过 + // 处理外部符号: 导入其他语言作为外部符号 auto external = ntrm->As(); if (external) { - // TODO: 外部非终结符需要从其他语言加载 - continue; - } - - // 跳过 ntrm.using,它是复用其他语言的非终结符 - // 在简化版中,我们不支持这个功能 - auto use = ntrm->As(); - if (use) { + auto grammarPath = external.grammar()->Text(); + // 去除引号 + grammarPath = grammarPath.substr(1, grammarPath.size() - 2); + + if (loader) { + auto externalSyntax = loader(grammarPath); + if (externalSyntax) { + syntax.Import(externalSyntax, external.name()->Text()); + } + } continue; } diff --git a/src/aliox/template.cpp b/src/aliox/template.cpp index d306eda..63992ac 100644 --- a/src/aliox/template.cpp +++ b/src/aliox/template.cpp @@ -4,6 +4,7 @@ #include "alioth/alioth.h" #include "alioth/parser.h" +#include "alioth/println_compat.h" #include "aliox/grammar.h" #include "fmt/format.h" #include "nlohmann/json.hpp"