lexer.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. #include "lexer.h"
  2. #include "runtime.h"
  3. #include <cctype>
  4. #include <functional>
  5. #include <map>
  6. #include <string>
  7. #include <vector>
  8. #define FILENAME "jinja-lexer"
  9. namespace jinja {
  10. static void string_lstrip(std::string & s, const char * chars) {
  11. size_t start = s.find_first_not_of(chars);
  12. if (start == std::string::npos) {
  13. s.clear();
  14. } else {
  15. s.erase(0, start);
  16. }
  17. }
  18. static void string_rstrip(std::string & s, const char * chars) {
  19. size_t end = s.find_last_not_of(chars);
  20. if (end == std::string::npos) {
  21. s.clear();
  22. } else {
  23. s.erase(end + 1);
  24. }
  25. }
  26. lexer_result lexer::tokenize(const std::string & source) {
  27. std::vector<token> tokens;
  28. // NOTE: do NOT transform the source string (i.e. preprocessing), as we need to keep
  29. // the original character positions for error reporting etc.
  30. std::string src = source;
  31. if (source.empty()) {
  32. return {tokens, src};
  33. }
  34. // Normalize \r\n or \r to \n
  35. for (std::string::size_type pos = 0; (pos = src.find("\r\n", pos)) != std::string::npos; ) {
  36. src.erase(pos, 1);
  37. ++pos;
  38. }
  39. for (std::string::size_type pos = 0; (pos = src.find("\r", pos)) != std::string::npos; ) {
  40. src.replace(pos, 1, 1, '\n');
  41. ++pos;
  42. }
  43. // In the default configuration:
  44. // - a single trailing newline is stripped if present
  45. // - other whitespace (spaces, tabs, newlines etc.) is returned unchanged
  46. if (source.back() == '\n') {
  47. src.pop_back();
  48. }
  49. size_t pos = 0;
  50. size_t start_pos = 0;
  51. size_t curly_bracket_depth = 0;
  52. using pred = std::function<bool(char)>;
  53. auto consume_while = [&](const pred & predicate) -> std::string {
  54. std::string str;
  55. while (predicate(src[pos])) {
  56. // check for escape char
  57. if (src[pos] == '\\') {
  58. // consume backslash
  59. ++pos;
  60. // check for end of input
  61. if (pos >= src.size()) {
  62. throw lexer_exception("unexpected end of input after escape character", source, pos);
  63. }
  64. // add escaped char
  65. char escaped_char = src[pos++];
  66. if (escape_chars.find(escaped_char) == escape_chars.end()) {
  67. throw lexer_exception(std::string("unknown escape character \\") + escaped_char, source, pos);
  68. }
  69. char unescaped_char = escape_chars.at(escaped_char);
  70. str += unescaped_char;
  71. continue;
  72. }
  73. str += src[pos++];
  74. if (pos > src.size()) {
  75. throw lexer_exception("unexpected end of input during consume_while", source, pos);
  76. }
  77. }
  78. return str;
  79. };
  80. auto next_pos_is = [&](std::initializer_list<char> chars, size_t n = 1) -> bool {
  81. if (pos + n >= src.size()) return false;
  82. for (char c : chars) {
  83. if (src[pos + n] == c) return true;
  84. }
  85. return false;
  86. };
  87. // note: default config for chat template: lstrip_blocks = true, trim_blocks = true
  88. // text\n[space]{block} --> text\n{block}
  89. bool opt_lstrip_blocks = true;
  90. // {block}\n[space]text --> {block}[space]text
  91. bool opt_trim_blocks = true;
  92. // options set dynamically based on current/last block
  93. bool is_lstrip_block = false; // example: {%-
  94. bool is_rstrip_block = false; // example: -%}
  95. while (pos < src.size()) {
  96. start_pos = pos;
  97. // JJ_DEBUG("lexer main loop at pos %zu: '%s...'", pos, src.substr(pos, 10).c_str());
  98. // First, consume all text that is outside of a Jinja statement or expression
  99. token::type last_token_type = tokens.empty()
  100. ? token::close_statement // initial state
  101. : tokens.back().t;
  102. if (last_token_type == token::close_statement ||
  103. last_token_type == token::close_expression ||
  104. last_token_type == token::comment) {
  105. bool last_block_can_rm_newline = false;
  106. is_rstrip_block = false;
  107. if (pos > 3) {
  108. char c0 = src[pos - 3];
  109. char c1 = src[pos - 2];
  110. char c2 = src[pos - 1];
  111. // strip if: -[%}#]}text
  112. is_rstrip_block = c0 == '-'
  113. && (c1 == '%' || c1 == '}' || c1 == '#')
  114. && c2 == '}';
  115. // match behavior of hf.js: exclude {{ and }} cases, regex: ([#%-]})
  116. last_block_can_rm_newline = (c1 == '#' || c1 == '%' || c1 == '-') && c2 == '}';
  117. }
  118. size_t start = pos;
  119. size_t end = start;
  120. while (pos < src.size() &&
  121. // Keep going until we hit the next Jinja statement or expression
  122. !(
  123. src[pos] == '{' &&
  124. next_pos_is( {'%', '{', '#'} )
  125. )) {
  126. end = ++pos;
  127. }
  128. // equivalent to hf.js code: template.replace(/^[ \t]*({[#%-])/gm, "$1");
  129. if (opt_lstrip_blocks && src[pos] == '{' && next_pos_is({'%', '#', '-'})) {
  130. size_t current = end;
  131. while (current > start) {
  132. char c = src[current - 1];
  133. if (current == 1) {
  134. end = 0; // Trim from the start of the string
  135. break;
  136. }
  137. if (c == '\n') {
  138. end = current; // Trim from the start of the line
  139. break;
  140. }
  141. if (!std::isspace(static_cast<unsigned char>(c))) {
  142. break; // Found non-whitespace before newline, keep
  143. }
  144. --current;
  145. }
  146. }
  147. std::string text = src.substr(start, end - start);
  148. // equivalent to hf.js code: template.replace(/([#%-]})\n/g, "$1");
  149. if (opt_trim_blocks && last_block_can_rm_newline) {
  150. if (!text.empty() && text.front() == '\n') {
  151. text.erase(text.begin());
  152. }
  153. }
  154. if (is_rstrip_block) {
  155. // example: {last_block}[space]text
  156. // doing lstrip on text, effectively rstrip the LAST block
  157. // JJ_DEBUG("RSTRIP block detected, current text: '%s'", text.c_str());
  158. string_lstrip(text, " \t\r\n");
  159. }
  160. is_lstrip_block = src[pos] == '{' && next_pos_is({'{', '%', '#'}) && next_pos_is({'-'}, 2);
  161. if (is_lstrip_block) {
  162. // example: text[space]{current_block}
  163. // doing rstrip on text, effectively lstrip the CURRENT block
  164. // JJ_DEBUG("LSTRIP block detected, current text: '%s'", text.c_str());
  165. string_rstrip(text, " \t\r\n");
  166. }
  167. if (!text.empty()) {
  168. // JJ_DEBUG("consumed text: '%s'", text.c_str());
  169. tokens.push_back({token::text, text, start_pos});
  170. continue;
  171. }
  172. }
  173. // Possibly consume a comment
  174. // TODO: handle lstrip/rstrip for comments? (not important for now)
  175. if (src[pos] == '{' && next_pos_is( {'#'} )) {
  176. start_pos = pos;
  177. pos += 2; // Skip the opening {#
  178. std::string comment;
  179. while (!(src[pos] == '#' && next_pos_is( {'}'} ))) {
  180. if (pos + 2 >= src.size()) {
  181. throw lexer_exception("missing end of comment tag", source, pos);
  182. }
  183. comment += src[pos++];
  184. }
  185. JJ_DEBUG("consumed comment: '%s'", comment.c_str());
  186. tokens.push_back({token::comment, comment, start_pos});
  187. pos += 2; // Skip the closing #}
  188. continue;
  189. }
  190. if (src[pos] == '-' && (
  191. last_token_type == token::open_expression ||
  192. last_token_type == token::open_statement)
  193. ) {
  194. JJ_DEBUG("lexer main loop at pos %zu: '%s...'", pos, src.substr(pos, 10).c_str());
  195. pos++; // consume '-' in {%- or {{-
  196. if (pos >= src.size()) break;
  197. }
  198. // Consume (and ignore) all whitespace inside Jinja statements or expressions
  199. consume_while([](char c) { return std::isspace(static_cast<unsigned char>(c)); });
  200. if (pos >= src.size()) break;
  201. char ch = src[pos];
  202. bool is_closing_block = ch == '-' && next_pos_is( {'%', '}'} );
  203. // Check for unary operators
  204. if (!is_closing_block && (ch == '-' || ch == '+')) {
  205. start_pos = pos;
  206. token::type last_token_type = tokens.empty() ? token::eof : tokens.back().t;
  207. if (last_token_type == token::text || last_token_type == token::eof) {
  208. throw lexer_exception(std::string("unexpected character: ") + ch, source, pos);
  209. }
  210. switch (last_token_type) {
  211. case token::identifier:
  212. case token::numeric_literal:
  213. case token::string_literal:
  214. case token::close_paren:
  215. case token::close_square_bracket:
  216. // Part of a binary operator
  217. // a - 1, 1 - 1, true - 1, "apple" - 1, (1) - 1, a[1] - 1
  218. // Continue parsing normally
  219. break;
  220. default: {
  221. // Is part of a unary operator
  222. // (-1), [-1], (1 + -1), not -1, -apple
  223. ++pos; // Consume the operator
  224. // Check for numbers following the unary operator
  225. std::string num = consume_while(is_integer);
  226. std::string value = std::string(1, ch) + num;
  227. token::type t = num.empty() ? token::unary_operator : token::numeric_literal;
  228. // JJ_DEBUG("consumed unary operator or numeric literal: '%s'", value.c_str());
  229. tokens.push_back({t, value, start_pos});
  230. continue;
  231. }
  232. }
  233. }
  234. // Try to match one of the tokens in the mapping table
  235. bool matched = false;
  236. for (const auto & [seq, typ] : ordered_mapping_table) {
  237. start_pos = pos;
  238. // Inside an object literal, don't treat "}}" as expression-end
  239. if (seq == "}}" && curly_bracket_depth > 0) {
  240. continue;
  241. }
  242. if (pos + seq.size() <= src.size() && src.substr(pos, seq.size()) == seq) {
  243. tokens.push_back({typ, seq, start_pos});
  244. if (typ == token::open_expression) {
  245. curly_bracket_depth = 0;
  246. } else if (typ == token::open_curly_bracket) {
  247. ++curly_bracket_depth;
  248. } else if (typ == token::close_curly_bracket) {
  249. --curly_bracket_depth;
  250. }
  251. pos += seq.size();
  252. matched = true;
  253. break; // continue main loop
  254. }
  255. }
  256. if (matched) continue; // continue main loop
  257. // Strings
  258. if (ch == '\'' || ch == '"') {
  259. start_pos = pos;
  260. ++pos; // Skip opening quote
  261. std::string str = consume_while([ch](char c) { return c != ch; });
  262. // JJ_DEBUG("consumed string literal: '%s'", str.c_str());
  263. tokens.push_back({token::string_literal, str, start_pos});
  264. ++pos; // Skip closing quote
  265. continue;
  266. }
  267. // Numbers
  268. if (is_integer(ch)) {
  269. start_pos = pos;
  270. std::string num = consume_while(is_integer);
  271. if (pos < src.size() && src[pos] == '.' && pos + 1 < src.size() && is_integer(src[pos + 1])) {
  272. ++pos; // Consume '.'
  273. std::string frac = consume_while(is_integer);
  274. num += "." + frac;
  275. }
  276. // JJ_DEBUG("consumed numeric literal: '%s'", num.c_str());
  277. tokens.push_back({token::numeric_literal, num, start_pos});
  278. continue;
  279. }
  280. // Identifiers
  281. if (is_word(ch)) {
  282. start_pos = pos;
  283. std::string word = consume_while(is_word);
  284. // JJ_DEBUG("consumed identifier: '%s'", word.c_str());
  285. tokens.push_back({token::identifier, word, start_pos});
  286. continue;
  287. }
  288. throw lexer_exception(std::string("unexpected character: ") + ch, source, pos);
  289. }
  290. return {std::move(tokens), src};
  291. }
  292. } // namespace jinja