ExpressionTokenizer.vala 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. using Invercargill.DataStructures;
  2. namespace Invercargill.Expressions {
  3. /**
  4. * Token types for expression parsing.
  5. */
  6. public enum TokenType {
  7. // Literals
  8. INTEGER,
  9. LONG_INTEGER, // integer with L suffix
  10. UNSIGNED_LONG, // integer with UL suffix
  11. FLOAT,
  12. FLOAT_LITERAL, // float with f suffix
  13. STRING,
  14. CHAR_LITERAL, // single character in single quotes
  15. TRUE,
  16. FALSE,
  17. NULL_LITERAL,
  18. // Identifiers and keywords
  19. IDENTIFIER,
  20. // Parameter placeholder
  21. PARAMETER, // $0, $1, $2, etc.
  22. // Operators
  23. PLUS, // +
  24. MINUS, // -
  25. STAR, // *
  26. SLASH, // /
  27. PERCENT, // %
  28. EQUALS, // ==
  29. NOT_EQUALS, // !=
  30. LESS_THAN, // <
  31. GREATER_THAN, // >
  32. LESS_EQUALS, // <=
  33. GREATER_EQUALS, // >=
  34. AND, // &&
  35. OR, // ||
  36. NOT, // !
  37. ASSIGN, // = (for single equals, used in some contexts)
  38. // Punctuation
  39. DOT, // .
  40. COMMA, // ,
  41. LPAREN, // (
  42. RPAREN, // )
  43. LBRACKET, // [
  44. RBRACKET, // ]
  45. QUESTION, // ?
  46. COLON, // :
  47. ARROW, // =>
  48. // Special
  49. EOF;
  50. public string to_string() {
  51. switch (this) {
  52. case INTEGER: return "INTEGER";
  53. case LONG_INTEGER: return "LONG_INTEGER";
  54. case UNSIGNED_LONG: return "UNSIGNED_LONG";
  55. case FLOAT: return "FLOAT";
  56. case FLOAT_LITERAL: return "FLOAT_LITERAL";
  57. case STRING: return "STRING";
  58. case CHAR_LITERAL: return "CHAR_LITERAL";
  59. case TRUE: return "TRUE";
  60. case FALSE: return "FALSE";
  61. case NULL_LITERAL: return "NULL";
  62. case IDENTIFIER: return "IDENTIFIER";
  63. case PARAMETER: return "PARAMETER";
  64. case PLUS: return "+";
  65. case MINUS: return "-";
  66. case STAR: return "*";
  67. case SLASH: return "/";
  68. case PERCENT: return "%";
  69. case EQUALS: return "==";
  70. case NOT_EQUALS: return "!=";
  71. case LESS_THAN: return "<";
  72. case GREATER_THAN: return ">";
  73. case LESS_EQUALS: return "<=";
  74. case GREATER_EQUALS: return ">=";
  75. case AND: return "&&";
  76. case OR: return "||";
  77. case NOT: return "!";
  78. case ASSIGN: return "=";
  79. case DOT: return ".";
  80. case COMMA: return ",";
  81. case LPAREN: return "(";
  82. case RPAREN: return ")";
  83. case LBRACKET: return "[";
  84. case RBRACKET: return "]";
  85. case QUESTION: return "?";
  86. case COLON: return ":";
  87. case ARROW: return "=>";
  88. case EOF: return "EOF";
  89. default: return "UNKNOWN";
  90. }
  91. }
  92. }
  93. /**
  94. * A token from the expression tokenizer.
  95. */
  96. public class Token : Object {
  97. public TokenType token_type { get; private set; }
  98. public string value { get; private set; }
  99. public int position { get; private set; }
  100. public Token(TokenType token_type, string value, int position) {
  101. this.token_type = token_type;
  102. this.value = value;
  103. this.position = position;
  104. }
  105. public string to_string() {
  106. return @"Token($(token_type.to_string()), \"$value\", pos=$position)";
  107. }
  108. }
  109. /**
  110. * Tokenizer for expression strings.
  111. *
  112. * Converts an expression string into a stream of tokens for parsing.
  113. */
  114. public class ExpressionTokenizer : Object {
  115. private string _input;
  116. private int _position;
  117. private int _length;
  118. /**
  119. * Creates a new tokenizer for the given input string.
  120. *
  121. * @param input The expression string to tokenize
  122. */
  123. public ExpressionTokenizer(string input) {
  124. _input = input;
  125. _position = 0;
  126. _length = input.length;
  127. }
  128. /**
  129. * Tokenizes the entire input and returns all tokens.
  130. *
  131. * @return A list of all tokens including the EOF token
  132. * @throws ExpressionError if tokenization fails
  133. */
  134. public Series<Token> tokenize_all() throws ExpressionError {
  135. var tokens = new Series<Token>();
  136. Token token;
  137. while ((token = next_token()).token_type != TokenType.EOF) {
  138. tokens.add(token);
  139. }
  140. tokens.add(token); // Add EOF token
  141. return tokens;
  142. }
  143. /**
  144. * Gets the next token from the input.
  145. *
  146. * @return The next token, or EOF if at end of input
  147. * @throws ExpressionError if tokenization fails
  148. */
  149. public Token next_token() throws ExpressionError {
  150. skip_whitespace();
  151. if (_position >= _length) {
  152. return new Token(TokenType.EOF, "", _position);
  153. }
  154. char c = _input[_position];
  155. int start_pos = _position;
  156. // Single character tokens
  157. switch (c) {
  158. case '+':
  159. _position++;
  160. return new Token(TokenType.PLUS, "+", start_pos);
  161. case '*':
  162. _position++;
  163. return new Token(TokenType.STAR, "*", start_pos);
  164. case '/':
  165. _position++;
  166. return new Token(TokenType.SLASH, "/", start_pos);
  167. case '%':
  168. _position++;
  169. return new Token(TokenType.PERCENT, "%", start_pos);
  170. case '.':
  171. _position++;
  172. return new Token(TokenType.DOT, ".", start_pos);
  173. case ',':
  174. _position++;
  175. return new Token(TokenType.COMMA, ",", start_pos);
  176. case '(':
  177. _position++;
  178. return new Token(TokenType.LPAREN, "(", start_pos);
  179. case ')':
  180. _position++;
  181. return new Token(TokenType.RPAREN, ")", start_pos);
  182. case '[':
  183. _position++;
  184. return new Token(TokenType.LBRACKET, "[", start_pos);
  185. case ']':
  186. _position++;
  187. return new Token(TokenType.RBRACKET, "]", start_pos);
  188. case '?':
  189. _position++;
  190. return new Token(TokenType.QUESTION, "?", start_pos);
  191. case ':':
  192. _position++;
  193. return new Token(TokenType.COLON, ":", start_pos);
  194. }
  195. // Two-character operators
  196. if (_position + 1 < _length) {
  197. string two_char = _input.substring(_position, 2);
  198. if (two_char == "==" ) {
  199. _position += 2;
  200. return new Token(TokenType.EQUALS, "==", start_pos);
  201. }
  202. if (two_char == "!=") {
  203. _position += 2;
  204. return new Token(TokenType.NOT_EQUALS, "!=", start_pos);
  205. }
  206. if (two_char == "<=") {
  207. _position += 2;
  208. return new Token(TokenType.LESS_EQUALS, "<=", start_pos);
  209. }
  210. if (two_char == ">=") {
  211. _position += 2;
  212. return new Token(TokenType.GREATER_EQUALS, ">=", start_pos);
  213. }
  214. if (two_char == "&&") {
  215. _position += 2;
  216. return new Token(TokenType.AND, "&&", start_pos);
  217. }
  218. if (two_char == "||") {
  219. _position += 2;
  220. return new Token(TokenType.OR, "||", start_pos);
  221. }
  222. if (two_char == "=>") {
  223. _position += 2;
  224. return new Token(TokenType.ARROW, "=>", start_pos);
  225. }
  226. }
  227. // Single character operators that might be part of two-char
  228. if (c == '<') {
  229. _position++;
  230. return new Token(TokenType.LESS_THAN, "<", start_pos);
  231. }
  232. if (c == '>') {
  233. _position++;
  234. return new Token(TokenType.GREATER_THAN, ">", start_pos);
  235. }
  236. if (c == '!') {
  237. _position++;
  238. return new Token(TokenType.NOT, "!", start_pos);
  239. }
  240. if (c == '=') {
  241. _position++;
  242. return new Token(TokenType.ASSIGN, "=", start_pos);
  243. }
  244. if (c == '-') {
  245. _position++;
  246. return new Token(TokenType.MINUS, "-", start_pos);
  247. }
  248. // Parameter placeholder ($0, $1, etc.)
  249. if (c == '$') {
  250. return read_parameter();
  251. }
  252. // String literals (double quotes) or char literals (single quotes)
  253. if (c == '"') {
  254. return read_string(c);
  255. }
  256. if (c == '\'') {
  257. return read_char_literal();
  258. }
  259. // Numbers
  260. if (c.isdigit()) {
  261. return read_number();
  262. }
  263. // Identifiers and keywords
  264. if (c.isalpha() || c == '_') {
  265. return read_identifier();
  266. }
  267. throw new ExpressionError.INVALID_SYNTAX(
  268. @"Unexpected character '$c' at position $(_position)"
  269. );
  270. }
  271. private void skip_whitespace() {
  272. while (_position < _length && _input[_position].isspace()) {
  273. _position++;
  274. }
  275. }
  276. private Token read_string(char quote) throws ExpressionError {
  277. int start_pos = _position;
  278. _position++; // Skip opening quote
  279. var sb = new StringBuilder();
  280. while (_position < _length) {
  281. char c = _input[_position];
  282. if (c == quote) {
  283. _position++; // Skip closing quote
  284. return new Token(TokenType.STRING, sb.str, start_pos);
  285. }
  286. if (c == '\\') {
  287. _position++;
  288. if (_position >= _length) {
  289. throw new ExpressionError.INVALID_SYNTAX(
  290. @"Unterminated string at position $start_pos"
  291. );
  292. }
  293. char escaped = _input[_position];
  294. switch (escaped) {
  295. case 'n': sb.append("\n"); break;
  296. case 't': sb.append("\t"); break;
  297. case 'r': sb.append("\r"); break;
  298. case '\\': sb.append("\\"); break;
  299. case '"': sb.append("\""); break;
  300. case '\'': sb.append("'"); break;
  301. case '%': sb.append("%"); break;
  302. case 'x':
  303. // Hex escape \xNN
  304. _position++;
  305. if (_position + 1 >= _length) {
  306. throw new ExpressionError.INVALID_SYNTAX(
  307. @"Invalid hex escape at position $(_position)"
  308. );
  309. }
  310. string hex = _input.substring(_position, 2);
  311. if (!hex[0].isxdigit() || !hex[1].isxdigit()) {
  312. throw new ExpressionError.INVALID_SYNTAX(
  313. @"Invalid hex escape '\\x$hex' at position $(_position)"
  314. );
  315. }
  316. int char_val = parse_hex(hex);
  317. sb.append_c((char)char_val);
  318. _position += 1; // Will be incremented again below
  319. break;
  320. default:
  321. throw new ExpressionError.INVALID_SYNTAX(
  322. @"Unknown escape sequence '\\$escaped' at position $(_position)"
  323. );
  324. }
  325. _position++;
  326. } else {
  327. sb.append_c(c);
  328. _position++;
  329. }
  330. }
  331. throw new ExpressionError.INVALID_SYNTAX(
  332. @"Unterminated string starting at position $start_pos"
  333. );
  334. }
  335. private Token read_char_literal() throws ExpressionError {
  336. int start_pos = _position;
  337. _position++; // Skip opening quote
  338. if (_position >= _length) {
  339. throw new ExpressionError.INVALID_SYNTAX(
  340. @"Unterminated character literal at position $start_pos"
  341. );
  342. }
  343. char c = _input[_position];
  344. char char_value;
  345. if (c == '\\') {
  346. // Escape sequence
  347. _position++;
  348. if (_position >= _length) {
  349. throw new ExpressionError.INVALID_SYNTAX(
  350. @"Unterminated character literal at position $start_pos"
  351. );
  352. }
  353. char escaped = _input[_position];
  354. switch (escaped) {
  355. case 'n': char_value = '\n'; break;
  356. case 't': char_value = '\t'; break;
  357. case 'r': char_value = '\r'; break;
  358. case '\\': char_value = '\\'; break;
  359. case '\'': char_value = '\''; break;
  360. case 'x':
  361. // Hex escape \xNN
  362. _position++;
  363. if (_position + 1 >= _length) {
  364. throw new ExpressionError.INVALID_SYNTAX(
  365. @"Invalid hex escape at position $(_position)"
  366. );
  367. }
  368. string hex = _input.substring(_position, 2);
  369. if (!hex[0].isxdigit() || !hex[1].isxdigit()) {
  370. throw new ExpressionError.INVALID_SYNTAX(
  371. @"Invalid hex escape '\\x$hex' at position $(_position)"
  372. );
  373. }
  374. char_value = (char)parse_hex(hex);
  375. _position++; // Extra increment for second hex digit
  376. break;
  377. default:
  378. throw new ExpressionError.INVALID_SYNTAX(
  379. @"Unknown escape sequence '\\$escaped' in character literal at position $(_position)"
  380. );
  381. }
  382. } else {
  383. char_value = c;
  384. }
  385. _position++;
  386. // Expect closing quote
  387. if (_position >= _length || _input[_position] != '\'') {
  388. throw new ExpressionError.INVALID_SYNTAX(
  389. @"Unterminated character literal starting at position $start_pos"
  390. );
  391. }
  392. _position++; // Skip closing quote
  393. return new Token(TokenType.CHAR_LITERAL, char_value.to_string(), start_pos);
  394. }
  395. private Token read_number() throws ExpressionError {
  396. int start_pos = _position;
  397. var sb = new StringBuilder();
  398. bool has_decimal = false;
  399. // Read integer part
  400. while (_position < _length && _input[_position].isdigit()) {
  401. sb.append_c(_input[_position]);
  402. _position++;
  403. }
  404. // Check for decimal point
  405. if (_position < _length && _input[_position] == '.') {
  406. // Look ahead to make sure it's not a property access
  407. if (_position + 1 < _length && _input[_position + 1].isdigit()) {
  408. has_decimal = true;
  409. sb.append_c('.');
  410. _position++;
  411. // Read decimal part
  412. while (_position < _length && _input[_position].isdigit()) {
  413. sb.append_c(_input[_position]);
  414. _position++;
  415. }
  416. }
  417. }
  418. string value = sb.str;
  419. // Check for type suffixes
  420. if (_position < _length) {
  421. char suffix = _input[_position].tolower();
  422. // Check for 'ul' or 'UL' suffix (unsigned long)
  423. if (suffix == 'u' && _position + 1 < _length && _input[_position + 1].tolower() == 'l') {
  424. _position += 2;
  425. return new Token(TokenType.UNSIGNED_LONG, value, start_pos);
  426. }
  427. // Check for 'l' suffix (long)
  428. if (suffix == 'l') {
  429. _position++;
  430. return new Token(TokenType.LONG_INTEGER, value, start_pos);
  431. }
  432. // Check for 'f' suffix (float)
  433. if (suffix == 'f') {
  434. _position++;
  435. return new Token(TokenType.FLOAT_LITERAL, value, start_pos);
  436. }
  437. }
  438. if (has_decimal) {
  439. return new Token(TokenType.FLOAT, value, start_pos);
  440. } else {
  441. return new Token(TokenType.INTEGER, value, start_pos);
  442. }
  443. }
  444. private Token read_identifier() {
  445. int start_pos = _position;
  446. var sb = new StringBuilder();
  447. while (_position < _length) {
  448. char c = _input[_position];
  449. if (c.isalnum() || c == '_') {
  450. sb.append_c(c);
  451. _position++;
  452. } else {
  453. break;
  454. }
  455. }
  456. string value = sb.str;
  457. // Check for keywords
  458. switch (value.down()) {
  459. case "true":
  460. return new Token(TokenType.TRUE, value, start_pos);
  461. case "false":
  462. return new Token(TokenType.FALSE, value, start_pos);
  463. case "null":
  464. return new Token(TokenType.NULL_LITERAL, value, start_pos);
  465. default:
  466. return new Token(TokenType.IDENTIFIER, value, start_pos);
  467. }
  468. }
  469. private Token read_parameter() throws ExpressionError {
  470. int start_pos = _position;
  471. _position++; // Skip $
  472. if (_position >= _length || !_input[_position].isdigit()) {
  473. throw new ExpressionError.INVALID_SYNTAX(
  474. @"Expected digit after '$$' at position $start_pos"
  475. );
  476. }
  477. var sb = new StringBuilder();
  478. while (_position < _length && _input[_position].isdigit()) {
  479. sb.append_c(_input[_position]);
  480. _position++;
  481. }
  482. return new Token(TokenType.PARAMETER, sb.str, start_pos);
  483. }
  484. private static int parse_hex(string hex) {
  485. int result = 0;
  486. for (int i = 0; i < hex.length; i++) {
  487. char c = hex[i];
  488. result *= 16;
  489. if (c >= '0' && c <= '9') {
  490. result += c - '0';
  491. } else if (c >= 'a' && c <= 'f') {
  492. result += c - 'a' + 10;
  493. } else if (c >= 'A' && c <= 'F') {
  494. result += c - 'A' + 10;
  495. }
  496. }
  497. return result;
  498. }
  499. }
  500. }