| 1 | <?php |
| 2 | namespace Std\Lib\Syntax; |
| 3 | |
| 4 | /** |
| 5 | * 言語定義の基本クラス |
| 6 | * C系の言語の既定値を持つので、各言語は違う所だけを上書きする |
| 7 | */ |
| 8 | abstract class Language implements SyntaxInterface { |
| 9 | /** @var string[] 一行コメントの開始 */ |
| 10 | protected array $comment = ['//']; |
| 11 | /** @var array|null 複数行コメントの[開始, 終了] */ |
| 12 | protected ?array $multiline = ['/*', '*/']; |
| 13 | /** 複数行コメントを入れ子に出来るか(Swift等) */ |
| 14 | protected bool $nestedComments = false; |
| 15 | /** @var string[] 文字列の引用符(「\」でエスケープ) */ |
| 16 | protected array $strings = ['"', "'"]; |
| 17 | /** 行頭の「#」をプリプロセッサとして扱うか */ |
| 18 | protected bool $preprocessor = false; |
| 19 | /** @var string[] 次の「<…>」をファイル名とするプリプロセッサ指令 */ |
| 20 | protected array $headerDirectives = []; |
| 21 | /** 大文字と小文字を区別しないか */ |
| 22 | protected bool $caseInsensitive = false; |
| 23 | /** 「名前(」を関数として扱うか */ |
| 24 | protected bool $functionCalls = true; |
| 25 | /** 数字の区切り文字(C++の「'」、Go等の「_」) */ |
| 26 | protected string $digitSep = "'"; |
| 27 | /** 識別子の正規表現 */ |
| 28 | protected string $identifier = '[\p{L}_][\p{L}\p{N}_]*'; |
| 29 | /** @var array 追加の規則(既定の規則より先に確認する) */ |
| 30 | protected array $rules = []; |
| 31 | /** |
| 32 | * @var array 種類 => 単語一覧 |
| 33 | * 先に書いた種類が優先される |
| 34 | */ |
| 35 | protected array $tokens = []; |
| 36 | |
| 37 | /** @var array 単語 => 種類 */ |
| 38 | protected array $words = []; |
| 39 | |
| 40 | public function prepare(string $code): void { |
| 41 | $this->words = $this->buildWords($this->tokens); |
| 42 | } |
| 43 | |
| 44 | public function getRules(): array { |
| 45 | $r = $this->rules; |
| 46 | |
| 47 | if ($this->multiline && !$this->nestedComments) { |
| 48 | [$s, $e] = $this->multiline; |
| 49 | $r[] = ['re' => preg_quote($s, '~').'.*?(?:'.preg_quote($e, '~').'|\z)', 'class' => 'comment']; |
| 50 | } |
| 51 | |
| 52 | if (!empty($this->comment)) { |
| 53 | $alts = implode('|', array_map(fn($c) => preg_quote($c, '~'), $this->comment)); |
| 54 | $r[] = ['re' => "(?:{$alts})[^\\n]*", 'class' => 'comment']; |
| 55 | } |
| 56 | |
| 57 | foreach ($this->strings as $q) { |
| 58 | $q = preg_quote($q, '~'); |
| 59 | $r[] = ['re' => "{$q}(?:[^{$q}\\\\\\n]|\\\\.)*{$q}?", 'class' => 'string']; |
| 60 | } |
| 61 | |
| 62 | if ($this->preprocessor) { |
| 63 | $r[] = ['re' => '#[ \t]*[A-Za-z_]\w*', 'class' => 'preprocessor', 'bol' => true]; |
| 64 | } |
| 65 | |
| 66 | $r[] = ['re' => $this->numberPattern(), 'class' => 'number']; |
| 67 | $r[] = ['re' => $this->identifier, 'class' => 'ident']; |
| 68 | |
| 69 | return $r; |
| 70 | } |
| 71 | |
| 72 | public function classify(string $word, array $ctx): ?string { |
| 73 | $key = $this->caseInsensitive ? strtolower($word) : $word; |
| 74 | if (isset($this->words[$key])) return $this->words[$key]; |
| 75 | if ($this->functionCalls && $ctx['next'] === '(') return 'function'; |
| 76 | return null; |
| 77 | } |
| 78 | |
| 79 | public function getNestedComment(): ?array { |
| 80 | return $this->nestedComments ? $this->multiline : null; |
| 81 | } |
| 82 | |
| 83 | public function getHeaderDirectives(): array { |
| 84 | return $this->headerDirectives; |
| 85 | } |
| 86 | |
| 87 | /** |
| 88 | * 種類 => 単語一覧から、単語 => 種類の表を作る |
| 89 | * 一覧が「ヘッダー名 => 単語一覧」の場合はそのまま全部を使う(C::prepare()で絞る) |
| 90 | * |
| 91 | * @param array $tokens 種類 => 単語一覧 |
| 92 | * @return array 単語 => 種類 |
| 93 | */ |
| 94 | protected function buildWords(array $tokens): array { |
| 95 | $words = []; |
| 96 | foreach ($tokens as $class => $list) { |
| 97 | foreach ($list as $k => $w) { |
| 98 | foreach (is_array($w) ? $w : [$w] as $word) { |
| 99 | $key = $this->caseInsensitive ? strtolower($word) : $word; |
| 100 | if (!isset($words[$key])) $words[$key] = $class; |
| 101 | } |
| 102 | } |
| 103 | } |
| 104 | return $words; |
| 105 | } |
| 106 | |
| 107 | /** |
| 108 | * C系の数字(16進数・2進数・8進数・浮動小数点・接尾辞) |
| 109 | * |
| 110 | * @return string 正規表現 |
| 111 | */ |
| 112 | protected function numberPattern(): string { |
| 113 | $s = $this->digitSep === '' ? '' : preg_quote($this->digitSep, '~').'?'; |
| 114 | $d = "\\d(?:{$s}\\d)*"; |
| 115 | $x = "[0-9A-Fa-f](?:{$s}[0-9A-Fa-f])*"; |
| 116 | return "(?:0[xX]{$x}(?:\\.(?:{$x})?)?(?:[pP][+-]?\\d+)?" |
| 117 | ."|0[bB][01](?:{$s}[01])*" |
| 118 | ."|0[oO][0-7](?:{$s}[0-7])*" |
| 119 | ."|(?:{$d}(?:\\.(?!\\.)(?:{$d})?)?|\\.\\d(?:{$s}\\d)*)(?:[eE][+-]?\\d+)?)" |
| 120 | ."(?:[A-Za-z_]\\w*)?"; |
| 121 | } |
| 122 | |
| 123 | /** |
| 124 | * ベクトル・行列型を作る(float → float2…float4, float2x2…float4x4) |
| 125 | * |
| 126 | * @param array $bases 基本の型 |
| 127 | * @param bool $matrix 行列型も作るか |
| 128 | * @return array 型の一覧 |
| 129 | */ |
| 130 | protected static function vectorTypes(array $bases, bool $matrix = true): array { |
| 131 | $out = []; |
| 132 | foreach ($bases as $b) { |
| 133 | $out[] = $b; |
| 134 | for ($i = 1; $i <= 4; $i++) { |
| 135 | $out[] = "{$b}{$i}"; |
| 136 | if ($matrix) for ($j = 1; $j <= 4; $j++) $out[] = "{$b}{$i}x{$j}"; |
| 137 | } |
| 138 | } |
| 139 | return $out; |
| 140 | } |
| 141 | |
| 142 | /** |
| 143 | * 「名前0」…「名前N」の一覧を作る(レジスター用) |
| 144 | * |
| 145 | * @param string|array $prefix 接頭辞 |
| 146 | * @param int $from 最初の番号 |
| 147 | * @param int $to 最後の番号 |
| 148 | * @return array |
| 149 | */ |
| 150 | protected static function numbered(string|array $prefix, int $from, int $to): array { |
| 151 | $out = []; |
| 152 | foreach ((array)$prefix as $p) for ($i = $from; $i <= $to; $i++) $out[] = "{$p}{$i}"; |
| 153 | return $out; |
| 154 | } |
| 155 | } |