Wyodrębnienie jednostek leksykalnych z kodu o zajętości 123 KB trwa ponad 20 sekund. Kod jest następujący:

  1. const TOKEN_STRING = 1;
  2. const TOKEN_COMMENT = 2;
  3. const TOKEN_OBJECT = 3;
  4. const TOKEN_MAP = 4;
  5. const TOKEN_DESCRIPTION = 6;
  6. const TOKEN_IF = 9;
  7. const TOKEN_ELSE = 10;
  8. const TOKEN_FOR = 11;
  9. const TOKEN_LEFT_PARENTHESIS = 15;
  10. const TOKEN_RIGHT_PARENTHESIS = 16;
  11. const TOKEN_INT = 20;
  12. const TOKEN_FLOAT = 21;
  13. const TOKEN_BOOL = 22;
  14. const TOKEN_COLON = 26;
  15. const TOKEN_SEMICOLON = 27;
  16. const TOKEN_DATA_TYPE = 39;
  17. const TOKEN_NAME = 32;
  18. const TOKEN_COMMA = 33;
  19. const TOKEN_LEFT_BRACE = 34;
  20. const TOKEN_RIGHT_BRACE = 35;
  21. // to 1 połowa typów jednostek, a jest jeszcze druga połowa
  22.  
  23. class Token
  24. {
  25. public $type;
  26. public $text;
  27. public function __construct($token, $sequence)
  28. {
  29. $this->type = $token;
  30. $this->text = $sequence;
  31. }
  32. }
  33.  
  34. class Parser
  35. {
  36. private $tokens = []; //tu będą nasze jednostki
  37. private $objects = []; //obiekty
  38. private $maps = []; //mapy
  39. private $object; //opracowywany obiekt
  40.  
  41. public function parse($input)
  42. {
  43. $lexer = new Lexer;
  44. $lexer->add('"(?:[^"\\\\]|\\.)*"', TOKEN_STRING);
  45. $lexer->add('//.*?\n', TOKEN_COMMENT);
  46. $lexer->add('OBJECT', TOKEN_OBJECT);
  47. $lexer->add('MAP', TOKEN_MAP);
  48. $lexer->add('DESCRIPTION', TOKEN_DESCRIPTION);
  49. $lexer->add('if', TOKEN_IF);
  50. $lexer->add('else', TOKEN_ELSE);
  51. $lexer->add('for', TOKEN_FOR);
  52. $lexer->add('string\b|int\b|float\b|bool\b', TOKEN_DATA_TYPE);
  53. $lexer->add('[0-9]+\\.[0-9]+', TOKEN_FLOAT);
  54. $lexer->add('0x[0-9A-F]+', TOKEN_INT);
  55. $lexer->add('[0-9]+', TOKEN_INT);
  56. $lexer->add('true|false', TOKEN_BOOL);
  57. $lexer->add('[a-zA-Z_$][a-zA-Z0-9_$]*', TOKEN_NAME); //zmienne
  58. //i cała reszta
  59.  
  60. $this->tokens = $lexer->tokenize($input);
  61. }
  62. }
  63.  
  64. class Lexer
  65. {
  66. private $regexs = [];
  67. public function add($regex, $token)
  68. {
  69. $this->regexs['~'.$regex.'~iA'] = $token; //flaga A to samo co ^ na początku
  70. }
  71. public function tokenize($input)
  72. {
  73. $tokens = [];
  74. $bom = pack('H*','EFBBBF');
  75. $input = preg_replace("/^$bom/", '', trim($input));
  76. while($input !== '')
  77. {
  78. $match = false;
  79. foreach($this->regexs as $regex=>$token)
  80. {
  81. if(preg_match($regex, $input, $matches))
  82. {
  83. $match = true;
  84. $input = trim(preg_replace($regex, '', $input, 1));
  85. if($token === TOKEN_COMMENT)
  86. {
  87. break; //rozwiązanie tymczasowe
  88. }
  89. elseif($token === TOKEN_STRING)
  90. {
  91. $matches[0] = substr($matches[0], 1, -1);
  92. }
  93. $tokens[] = new Token($token, $matches[0]);
  94. break;
  95. }
  96. }
  97. if(!$match)
  98. {
  99. throw new \Exception(sprintf('Invalid character %s', $input));
  100. }
  101. }
  102. return $tokens;
  103. }
  104. }

Jak widać, skrypt używa wyrażeń regularnych do wyodrębniania jednostek leksykalnych w natępujący sposób:

1. Wczytaj kod.
2. Dopóki kod nie jest pusty:
2.1. Przejedź po wszystkich zdefiniowanych typach jednostek:
2.1.1. Jeśli jednostka jest na początku kodu, wyodrębnij ją i usuń z kodu.
2.1.2. Jeśli nie dopasowano żadnej jednostki, rzuć wyjątek.

Czas zależy od liczby zdefiniowanych typów jednostek i ilości jednostek w analizowanym kodzie.

Większość jednostek można znaleźć za pomocą funkcji str_* ale czy uzyskamy duże przyspieszenie w stosunku do wyrażeń regularnych? Pojawi się wtedy inny problem.

Kod
int INTRO

Zakładając kolejność i nierozróżnianie wielkości znaków:

Kod
TYP : string|int|float|bool
NAZWA : [a-zA-Z_$][a-zA-Z0-9_$]*

Uzyskamy 3 jednostki - int (typ), INT (typ), RO (nazwa)
Na odwrót - int (nazwa), INTRO (nazwa)

Oba wyniki są błędne. Na szczęście PCRE wykrywa krawędzie słów za pomocą \b. Bez PCRE trzeba by badać to ręcznie. A może to błędne podejście do budowy analizatora leksykalnego? Jak go należy prawidłowo napisać?

Czy potrzebujemy aż tyle jednostek? Może wystarczy tylko kilka:
1) ciąg znaków (wszystko w cudzysłowach)
2) stała / zmienna (tekst bez cudzysłowów)
3) liczba
4) operatory - można rozbić na osobne typy jednostek
5) przecinek, nawiasy - jak wyżej

Co do (2) to byłoby już zadanie parsera, aby wykrywał słowa kluczowe.