From 23e32648918d9cbea8c6e4f69c15e1d0431b9640 Mon Sep 17 00:00:00 2001 From: f-tester Date: Sun, 4 Oct 2026 10:36:04 +0000 Subject: [PATCH 1/2] fix(lsp): answer while stdin is open and decode \uXXXX in JSON `flang lsp` and `--mcp-mode` read stdin with fread(chunk, 1, 8192, stdin), which returns only after 8192 bytes or end of input. An editor never closes the server's stdin, so the reply to `initialize` (334 bytes) never left: 0 bytes while the input is open, 334 after it closes. Read with read(2), which returns what has arrived (repl_read_some). run_text rejected any JSON string with \uXXXX, so the server dropped such a message with "unparsed JSON" on stderr. Decode \uXXXX and surrogate pairs to UTF-8; a lone half of a pair is still rejected. The printed copy in bootstrap/ comes in the next commit, from a reprint (scripts/bootstrap-reprint.sh), not by hand. scripts/editors/lsp-check.fscript now gates on the binary answering while stdin is open (the Node server it gated on is gone, so the gate passed vacuously) and runs Neovim on bootstrap/flang with g:flang_dvoichnyy_lsp = 1. Its expected definition line was 6; the declaration in probe-transition.flang is on line 3. The check never reached that step before, because Neovim found no server. The binary stays opt-in in autoload/flang.vim: released binaries up to 0.7.24 still hang. README and the autoload comment say so. docs/tree-inventory.md: five line counts remeasured. scripts/ledgers/proved-share-ledger.txt: lsp-check.fscript remeasured with --proof (21 written: 17 proved, 4 declared; the old line said 23/23 for a file whose md5 had already changed). Refs #294 --- docs/editors/vim/README.md | 29 ++++--- docs/editors/vim/autoload/flang.vim | 30 ++++--- docs/tree-inventory.md | 10 +-- flang/src/emit/c/flang_repl.c | 105 +++++++++++++++++++++--- scripts/editors/lsp-check.fscript | 34 +++----- scripts/ledgers/proved-share-ledger.txt | 2 +- 6 files changed, 147 insertions(+), 63 deletions(-) diff --git a/docs/editors/vim/README.md b/docs/editors/vim/README.md index 6917ba047..92cff6916 100644 --- a/docs/editors/vim/README.md +++ b/docs/editors/vim/README.md @@ -68,22 +68,27 @@ JavaScript (`fe8e8a37`), обёртка на Node — вместе с npm 6 се ### Почему двоичного `flang lsp` в этом списке нет -У двоичного `flang` (Homebrew, asdf) подкоманда `lsp` есть, и на закрытом вводе -она отвечает правильно. **Редактору она не годится: пока стандартный ввод -открыт, сервер не присылает ни одного байта.** Померено `checks/stream.sh`: +У двоичного `flang` (Homebrew, asdf) подкоманда `lsp` есть. **Выпуски по 0.7.24 +включительно редактору не годятся: пока стандартный ввод открыт, сервер не +присылает ни одного байта** — ввод читался кусками по 8192 байта, и чтение +ждало либо полный кусок, либо конец ввода. Померено `checks/stream.sh`: ``` -двоичный flang lsp: при открытом вводе 0 байт, после закрытия 334 байт -flang-lsp на Node: при открытом вводе 334 байт, после закрытия 334 байт +двоичный flang lsp 0.7.24: при открытом вводе 0 байт, после закрытия 334 байт +двоичный flang lsp из дерева: при открытом вводе 334 байт, после закрытия 334 байт ``` -Редактор ввод не закрывает никогда, поэтому двоичный сервер выглядел бы просто -висящим. Взять его всё равно можно — `let g:flang_dvoichnyy_lsp = 1`, — но по -умолчанию он не берётся: молчащий сервер неотличим от сломанного. - -Когда двоичный научится отвечать на лету, `scripts/editors/lsp-check.fscript` скажет -об этом строкой, и порядок поиска можно будет переставить. Тогда Node для -редактора станет не нужен вовсе. +Ввод теперь читается тем, что уже пришло (`repl_read_some` в +`flang/src/emit/c/flang_repl.c`), и строки JSON с `\uXXXX` разбираются — прежде +такое сообщение сервер выбрасывал. `scripts/editors/lsp-check.fscript` меряет +это первым шагом и гоняет Neovim на двоичном из дерева (`bootstrap/flang`): +диагностика, переход к объявлению, наведение. + +По умолчанию двоичный всё равно не берётся: у людей стоят выпуски по 0.7.24 +включительно, и такой сервер выглядел бы просто висящим, а молчащий сервер +неотличим от сломанного. Взять его — `let g:flang_dvoichnyy_lsp = 1`. Когда +выйдет выпуск с исправлением, порядок поиска можно будет переставить, и Node +для редактора станет не нужен вовсе. ### Клиент для Vim 8/9 — vim-lsp, и вот почему именно он diff --git a/docs/editors/vim/autoload/flang.vim b/docs/editors/vim/autoload/flang.vim index b7406422e..c32a080ca 100644 --- a/docs/editors/vim/autoload/flang.vim +++ b/docs/editors/vim/autoload/flang.vim @@ -7,14 +7,15 @@ " " ── Порядок путей и почему двоичный НЕ первый ─────────────────────────────── " -" У двоичного `flang` подкоманда `lsp` есть, и на закрытом вводе она отвечает -" правильно. Редактору она не годится: пока стандартный ввод открыт, сервер не -" присылает НИ ОДНОГО байта, и весь ответ вываливается разом при закрытии -" ввода. Померено (`docs/editors/vim/checks/potok.md`): с открытым вводом 0 байт -" за 3 секунды, после закрытия — 334 байта сразу; `flang-lsp` на Node на том же -" сообщении отвечает 311 байтами за 0,1 секунды. Редактор ввод не закрывает -" никогда, поэтому двоичный сервер выглядел бы как молча висящий, а молчащий -" сервер неотличим от сломанного. +" У двоичного `flang` подкоманда `lsp` есть. Выпуски по 0.7.24 включительно +" отвечают только после закрытия стандартного ввода: ввод читался кусками по +" 8192 байта, и `fread` ждал либо полный кусок, либо конец ввода. Померено +" `docs/editors/vim/checks/stream.sh`: с открытым вводом 0 байт, после +" закрытия — 334 байта сразу. Редактор ввод не закрывает никогда, поэтому у +" такого выпуска сервер выглядит молча висящим, а молчащий сервер неотличим от +" сломанного. Ввод теперь читается тем, что уже пришло (`repl_read_some` в +" `flang/src/emit/c/flang_repl.c`), и тот же замер даёт 334 байта при открытом +" вводе. " " Поэтому порядок такой: " @@ -29,9 +30,11 @@ " (`fe8e8a37`), обёртка `flang-lsp` — вместе с npm 6 сентября (задача 8649); " искать его редактор перестал 9 сентября (задача 6201). " -" Пункт 4 останется выключенным, пока двоичный не научится отвечать на лету. -" Когда научится — проверка `scripts/editors/lsp-check.fscript` позеленеет, и пункт -" можно будет поднять наверх: тогда Node для редактора станет не нужен. +" Пункт 4 остаётся по явной просьбе, хотя двоичный из этого дерева отвечает на +" лету: проверка `scripts/editors/lsp-check.fscript` меряет это первым шагом и +" гоняет Neovim именно на нём. Поставленные у людей выпуски по 0.7.24 +" включительно по-прежнему молчат, и взять такой без спроса значило бы показать +" висящий сервер. Поднять пункт наверх можно, когда выйдет выпуск с исправлением. let s:umeet_lsp = {} let s:skazano = 0 @@ -108,8 +111,9 @@ function! flang#Pochemu() abort \ ] if flang#DvoichnyyEst() call add(l:stroki, 'Двоичный flang с подкомандой lsp рядом ЕСТЬ, но он не взят:') - call add(l:stroki, ' он отвечает только после закрытия ввода, а редактор ввод не закрывает,') - call add(l:stroki, ' и сервер выглядел бы висящим. Взять всё равно: let g:flang_dvoichnyy_lsp = 1') + call add(l:stroki, ' выпуски по 0.7.24 включительно отвечают только после закрытия ввода, а редактор') + call add(l:stroki, ' ввод не закрывает, и сервер выглядел бы висящим; более новые отвечают сразу.') + call add(l:stroki, ' Взять его: let g:flang_dvoichnyy_lsp = 1') endif call add(l:stroki, 'Подсветка работает и без сервера.') return join(l:stroki, "\n") diff --git a/docs/tree-inventory.md b/docs/tree-inventory.md index f2360f766..3d671b677 100644 --- a/docs/tree-inventory.md +++ b/docs/tree-inventory.md @@ -77,7 +77,7 @@ $ bootstrap/flang io scripts/guards/tree-inventory.fscript --max-steps 50000000 | язык | файлов | строк | долг файлов | долг строк | |---|---:|---:|---:|---:| | оболочка | 58 | 10 073 | 52 | 5 629 | -| C | 37 | 882 220 | 0 | 0 | +| C | 37 | 882 386 | 0 | 0 | | C++ | 1 | 404 | 0 | 0 | | Python | 5 | 2 906 | 0 | 0 | | HTML | 7 | 1 263 | 0 | 0 | @@ -91,7 +91,7 @@ $ bootstrap/flang io scripts/guards/tree-inventory.fscript --max-steps 50000000 | Go | 3 | 2 880 | 0 | 0 | | Rust | 3 | 3 961 | 0 | 0 | | Lua | 3 | 225 | 0 | 0 | -| Vimscript | 8 | 390 | 0 | 0 | +| Vimscript | 8 | 394 | 0 | 0 | | Ruby | 1 | 113 | 0 | 0 | @@ -162,11 +162,11 @@ flang в C. | довод | что под ним | файлов | строк | |---|---|---:|---:| -| напечатано самим компилятором | `bootstrap/**` — шесть файлов на C, вывод печати | 6 | 830 152 | -| рантайм цели печати | `flang/src/emit/{c,cpp,python,java,csharp,elixir,go,rust}/**` — уезжает в напечатанную программу дословно | 29 | 52 183 | +| напечатано самим компилятором | `bootstrap/**` — шесть файлов на C, вывод печати | 6 | 830 235 | +| рантайм цели печати | `flang/src/emit/{c,cpp,python,java,csharp,elixir,go,rust}/**` — уезжает в напечатанную программу дословно | 29 | 52 266 | | замеряемый материал | `benchmarks/**` без оболочки и без оснастки на Python (`tasks.py` — набор задач замера, он материал) плюс `flang/concurrency/bench/beam.erl` — это то, с чем сравнивают | 12 | 3 217 | | проба рантайма C | `flang/test/oblast/*.c` и `flang/concurrency/bench/sizes.c` — двенадцать проб памяти, их заголовок и размеры записей; на flang они проверяли бы не рантайм, а себя. Гоняет их `scripts/targets/region-in-c-target.flang`, уже написанный на flang | 14 | 674 | -| чужая среда: редактор | `docs/editors/vim/**` — vim и neovim грузят только vimscript и Lua | 11 | 615 | +| чужая среда: редактор | `docs/editors/vim/**` — vim и neovim грузят только vimscript и Lua | 11 | 619 | | код на стороне цели | `flang/concurrency/bin/node.{c,cs,ex,go,java,py,rs}` — семь хозяев узлов на семи языках | 8 | 8 227 | | разметка и оформление | четыре `.html` и `docs/site/style.css` — цели HTML у языка нет и не заявлено | 5 | 1 785 | | чужая среда: установщик | `packaging/homebrew/flang.rb` и три файла `packaging/asdf/bin/` — homebrew понимает Ruby, asdf зовёт свои три раньше, чем flang в системе есть | 4 | 470 | diff --git a/flang/src/emit/c/flang_repl.c b/flang/src/emit/c/flang_repl.c index 1a76e8b89..bec07e0af 100644 --- a/flang/src/emit/c/flang_repl.c +++ b/flang/src/emit/c/flang_repl.c @@ -9852,11 +9852,77 @@ static void run_spaces(const char *text, size_t *at) { } } +/* Четыре шестнадцатеричные цифры `\uXXXX` с места `at`; -1 — не цифры. */ +static long run_hex4(const char *text, size_t at, size_t end) { + long value = 0; + size_t index = 0; + if (at + 4 > end) { + return -1; + } + for (index = at; index < at + 4; index += 1) { + const char symbol = text[index]; + value <<= 4; + if (symbol >= '0' && symbol <= '9') { + value |= symbol - '0'; + } else if (symbol >= 'a' && symbol <= 'f') { + value |= symbol - 'a' + 10; + } else if (symbol >= 'A' && symbol <= 'F') { + value |= symbol - 'A' + 10; + } else { + return -1; + } + } + return value; +} + +/* + * `\uXXXX` (и пара `\uD8xx\uDCxx`) в UTF-8. `*index` стоит сразу за `u`; + * буфера хватает всегда: шесть знаков дают не больше трёх байт, двенадцать — + * четыре. + */ +static bool run_text_unicode(const char *text, size_t *index, size_t end, char *buffer, size_t *used) { + long code = run_hex4(text, *index, end); + if (code < 0) { + return false; + } + *index += 4; + if (code >= 0xD800 && code <= 0xDBFF) { + long low = -1; + if (*index + 6 > end || text[*index] != '\\' || text[*index + 1] != 'u') { + return false; + } + low = run_hex4(text, *index + 2, end); + if (low < 0xDC00 || low > 0xDFFF) { + return false; + } + *index += 6; + code = 0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00); + } else if (code >= 0xDC00 && code <= 0xDFFF) { + return false; + } + if (code < 0x80) { + buffer[(*used)++] = (char)code; + } else if (code < 0x800) { + buffer[(*used)++] = (char)(0xC0 | (code >> 6)); + buffer[(*used)++] = (char)(0x80 | (code & 0x3F)); + } else if (code < 0x10000) { + buffer[(*used)++] = (char)(0xE0 | (code >> 12)); + buffer[(*used)++] = (char)(0x80 | ((code >> 6) & 0x3F)); + buffer[(*used)++] = (char)(0x80 | (code & 0x3F)); + } else { + buffer[(*used)++] = (char)(0xF0 | (code >> 18)); + buffer[(*used)++] = (char)(0x80 | ((code >> 12) & 0x3F)); + buffer[(*used)++] = (char)(0x80 | ((code >> 6) & 0x3F)); + buffer[(*used)++] = (char)(0x80 | (code & 0x3F)); + } + return true; +} + /* * Строка JSON в память арены. Escape-последовательности разбираются те, что - * есть в самом JSON; `\uXXXX` намеренно НЕ разбирается, и это не лень: ключи и - * значения приезжают из командной строки, где UTF-8 уже написан буквами, а - * половинчатая поддержка суррогатных пар хуже честного отказа. + * есть в самом JSON, и `\uXXXX` тоже — с суррогатными парами (`run_text_unicode`): + * языковой сервер получает JSON от редактора, а клиент вправе экранировать + * любой знак. Одинокая половина пары — отказ, а не подмена знаком. */ static bool run_text(const char *text, size_t *at, char **out, size_t *bytes) { size_t start = 0; @@ -9891,8 +9957,11 @@ static bool run_text(const char *text, size_t *at, char **out, size_t *bytes) { } else if (next == 'r') { buffer[used] = '\r'; } else if (next == 'u') { - free(buffer); - return false; + if (!run_text_unicode(text, &index, *at, buffer, &used)) { + free(buffer); + return false; + } + continue; } else { buffer[used] = next; } @@ -20059,6 +20128,20 @@ static char *lsp_path_of(const char *uri, const char *base) { return result; } +/* + * Сколько уже пришло на стандартный ввод — не больше `size` байт, но и не + * дожидаясь их: `fread` ждёт полный кусок или конец ввода, а редактор ввод не + * закрывает никогда, и ответ на первую же короткую рамку не уходил бы вовсе. + * Ноль — конец ввода, отрицательное — ошибка чтения. + */ +static ssize_t repl_read_some(char *chunk, size_t size) { + ssize_t got = read(STDIN_FILENO, chunk, size); + while (got < 0 && errno == EINTR) { + got = read(STDIN_FILENO, chunk, size); + } + return got; +} + /* ─────────────────────────── рамки Content-Length ───────────────────────── */ /* @@ -20127,11 +20210,11 @@ static char *lsp_read_frame(repl_buf *tail, size_t *bytes) { } { char chunk[8192]; - const size_t got = fread(chunk, 1, sizeof(chunk), stdin); - if (got == 0) { + const ssize_t got = repl_read_some(chunk, sizeof(chunk)); + if (got <= 0) { return NULL; } - buf_add(tail, chunk, got); + buf_add(tail, chunk, (size_t)got); } } } @@ -20562,11 +20645,11 @@ static char *mcp_read_line(repl_buf *tail, size_t *bytes) { } { char chunk[8192]; - const size_t got = fread(chunk, 1, sizeof(chunk), stdin); - if (got == 0) { + const ssize_t got = repl_read_some(chunk, sizeof(chunk)); + if (got <= 0) { return NULL; } - buf_add(tail, chunk, got); + buf_add(tail, chunk, (size_t)got); } } } diff --git a/scripts/editors/lsp-check.fscript b/scripts/editors/lsp-check.fscript index 3d86a8c91..eacf3190c 100644 --- a/scripts/editors/lsp-check.fscript +++ b/scripts/editors/lsp-check.fscript @@ -8,6 +8,8 @@ "--cmd", "set rtp^=../../docs/editors/vim", "--cmd", "filetype plugin on", "--cmd", "syntax on", + "--cmd", "let g:flang_dvoichnyy_lsp = 1", + "--cmd", "let $PATH = fnamemodify('../../bootstrap', ':p') . ':' . $PATH", "--cmd", "lua require('flang').setup()" ] как акк и довод добавить довод к акк @@ -24,7 +26,7 @@ возвращает список строки «Общие доводы» от [ "--cmd", "let g:flang_imya='«Удвоить»'", - "--cmd", "let g:flang_stroka=6", + "--cmd", "let g:flang_stroka=3", "-c", "lua dofile('../../docs/editors/vim/checks/lsp.lua').progon()", "../../docs/editors/vim/checks/probe-transition.flang" ] @@ -58,25 +60,15 @@ ожидается нет ответ содержит "ИТОГ: сошлось" -тотальная функция «Node отвечает на лету» - принимает замер: строка - возвращает признак - обеспечивает «молчание видно» (не (замер содержит "flang-lsp на Node: при открытом вводе 0 байт")) или (не результат) - обеспечивает «да — значит не молчит» (не результат) или (не (замер содержит "flang-lsp на Node: при открытом вводе 0 байт")) - пример «Отвечает» - дано замер равно "flang-lsp на Node: при открытом вводе 334 байт, после закрытия 334 байт" - ожидается да - пример «Молчит» - дано замер равно "flang-lsp на Node: при открытом вводе 0 байт, после закрытия 334 байт" - ожидается нет - не (замер содержит "flang-lsp на Node: при открытом вводе 0 байт") - тотальная функция «Двоичный отвечает на лету» принимает замер: строка возвращает признак обеспечивает «молчание видно» (не (замер содержит "двоичный flang lsp: при открытом вводе 0 байт")) или (не результат) обеспечивает «да — значит не молчит» (не результат) или (не (замер содержит "двоичный flang lsp: при открытом вводе 0 байт")) - пример «Пока молчит» + пример «Отвечает» + дано замер равно "двоичный flang lsp: при открытом вводе 334 байт, после закрытия 334 байт" + ожидается да + пример «Молчит» дано замер равно "двоичный flang lsp: при открытом вводе 0 байт, после закрытия 334 байт" ожидается нет не (замер содержит "двоичный flang lsp: при открытом вводе 0 байт") @@ -85,12 +77,12 @@ принимает замер: строка возвращает строка обеспечивает «первая ветвь — Строка про двоичный» если «Двоичный отвечает на лету» от замер то (результат равен ("двоичный отвечает на лету — поиск сервера можно переставить, Node больше не нужен")) иначе да - обеспечивает «вторая ветвь — Строка про двоичный» если не («Двоичный отвечает на лету» от замер) то (результат равен ("двоичный копит ответы до закрытия ввода — редактору не годится, поиск идёт через Node")) иначе да + обеспечивает «вторая ветвь — Строка про двоичный» если не («Двоичный отвечает на лету» от замер) то (результат равен ("двоичный копит ответы до закрытия ввода — редактору не годится")) иначе да обеспечивает «отвечает — так и сказано» (не («Двоичный отвечает на лету» от замер)) или (результат равен "двоичный отвечает на лету — поиск сервера можно переставить, Node больше не нужен") - обеспечивает «молчит — так и сказано» («Двоичный отвечает на лету» от замер) или (результат равен "двоичный копит ответы до закрытия ввода — редактору не годится, поиск идёт через Node") + обеспечивает «молчит — так и сказано» («Двоичный отвечает на лету» от замер) или (результат равен "двоичный копит ответы до закрытия ввода — редактору не годится") если «Двоичный отвечает на лету» от замер то "двоичный отвечает на лету — поиск сервера можно переставить, Node больше не нужен" - иначе "двоичный копит ответы до закрытия ввода — редактору не годится, поиск идёт через Node" + иначе "двоичный копит ответы до закрытия ввода — редактору не годится" тотальная функция «Vim 8 сошёлся или честно пропущен» принимает ответ: строка @@ -127,12 +119,12 @@ тотальная функция «После замера» принимает отклик: «Отклик» возвращает «Продолжение» - обеспечивает «молчание — провал» («Node отвечает на лету» от («Ответ процесса» от отклик)) или (результат равен (вариант «Провал» с код равным "FLANG_LSP_NE_POTOK" и сообщение равным (соединить ["сервер на Node перестал отвечать при открытом вводе:\n", («Ответ процесса» от отклик)] по ""))) + обеспечивает «молчание — провал» («Двоичный отвечает на лету» от («Ответ процесса» от отклик)) или (результат равен (вариант «Провал» с код равным "FLANG_LSP_NE_POTOK" и сообщение равным (соединить ["двоичный flang lsp не отвечает при открытом вводе:\n", («Ответ процесса» от отклик)] по ""))) пусть замер равно «Ответ процесса» от отклик - если «Node отвечает на лету» от замер + если «Двоичный отвечает на лету» от замер то вариант «Сделать» с поручение равным (вариант «Запустить процесс» с программа равным "nvim" и аргументы равным «Доводы беды») и потом равным (вариант «Беда показана» с замер равным замер) иначе вариант «Провал» с код равным "FLANG_LSP_NE_POTOK" и сообщение равным (соединить [ - "сервер на Node перестал отвечать при открытом вводе:\n", замер] по "") + "двоичный flang lsp не отвечает при открытом вводе:\n", замер] по "") тотальная функция «После беды» принимает замер: строка, отклик: «Отклик» diff --git a/scripts/ledgers/proved-share-ledger.txt b/scripts/ledgers/proved-share-ledger.txt index d5698bb3e..ff692953c 100644 --- a/scripts/ledgers/proved-share-ledger.txt +++ b/scripts/ledgers/proved-share-ledger.txt @@ -1524,7 +1524,7 @@ ea0c808c837ad28c1ad06045e2c03504|58|55|3|0|0|packaging/install-check.flang a69c24d54cb5ba2dc5e5f1ac2b5475e5|23|23|0|0|0|packaging/install-parity.flang e0822c51ca487deb59d8925c20396e93|7|7|0|0|0|scripts/cell-work-preserved.fscript 986fad97002d55a23e04a3dca541f8a5|1|1|0|0|0|scripts/editors/language-words.flang -764ea6a077d30318d14a8a0f3da8aea8|23|23|0|0|0|scripts/editors/lsp-check.fscript +1838287b42f3cd832187265d3edffc40|21|17|0|4|0|scripts/editors/lsp-check.fscript 53f94937b541c4d0ce9569351f7f23ec|11|11|0|0|0|scripts/editors/vim-highlight-check.fscript e3e12eb0215c5a68744c935ef6ee0619|9|9|0|0|0|scripts/editors/vim-highlighting.fscript 804182bd08fb527a43934b823e6d3de1|16|16|0|0|0|scripts/editors/vim-rules.flang From a528ef18e436ac57fbce43b9ee7e6e88b334e53b Mon Sep 17 00:00:00 2001 From: f-tester Date: Sun, 4 Oct 2026 14:35:46 +0000 Subject: [PATCH 2/2] build(seed): reprint the seed for the LSP read fix The seed is printed from 23e3264 by the GPU-cache build on the reprint script (code 0, 3205 s). Of the seven files only bootstrap/flang_repl.c changed, which is the file the fix touches; the other six are byte for byte as before. The printed seed was built and asked before it went into bootstrap/, and scripts/seed-fingerprint names commit 23e3264. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_019LuK84yUqX31p5tyBKcaTJ --- bootstrap/flang_repl.c | 105 +++++++++++++++++++++++++++++++++++---- scripts/seed-fingerprint | 6 +-- 2 files changed, 97 insertions(+), 14 deletions(-) diff --git a/bootstrap/flang_repl.c b/bootstrap/flang_repl.c index 6efdea6f3..22c07f5a3 100644 --- a/bootstrap/flang_repl.c +++ b/bootstrap/flang_repl.c @@ -9861,11 +9861,77 @@ static void run_spaces(const char *text, size_t *at) { } } +/* Четыре шестнадцатеричные цифры `\uXXXX` с места `at`; -1 — не цифры. */ +static long run_hex4(const char *text, size_t at, size_t end) { + long value = 0; + size_t index = 0; + if (at + 4 > end) { + return -1; + } + for (index = at; index < at + 4; index += 1) { + const char symbol = text[index]; + value <<= 4; + if (symbol >= '0' && symbol <= '9') { + value |= symbol - '0'; + } else if (symbol >= 'a' && symbol <= 'f') { + value |= symbol - 'a' + 10; + } else if (symbol >= 'A' && symbol <= 'F') { + value |= symbol - 'A' + 10; + } else { + return -1; + } + } + return value; +} + +/* + * `\uXXXX` (и пара `\uD8xx\uDCxx`) в UTF-8. `*index` стоит сразу за `u`; + * буфера хватает всегда: шесть знаков дают не больше трёх байт, двенадцать — + * четыре. + */ +static bool run_text_unicode(const char *text, size_t *index, size_t end, char *buffer, size_t *used) { + long code = run_hex4(text, *index, end); + if (code < 0) { + return false; + } + *index += 4; + if (code >= 0xD800 && code <= 0xDBFF) { + long low = -1; + if (*index + 6 > end || text[*index] != '\\' || text[*index + 1] != 'u') { + return false; + } + low = run_hex4(text, *index + 2, end); + if (low < 0xDC00 || low > 0xDFFF) { + return false; + } + *index += 6; + code = 0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00); + } else if (code >= 0xDC00 && code <= 0xDFFF) { + return false; + } + if (code < 0x80) { + buffer[(*used)++] = (char)code; + } else if (code < 0x800) { + buffer[(*used)++] = (char)(0xC0 | (code >> 6)); + buffer[(*used)++] = (char)(0x80 | (code & 0x3F)); + } else if (code < 0x10000) { + buffer[(*used)++] = (char)(0xE0 | (code >> 12)); + buffer[(*used)++] = (char)(0x80 | ((code >> 6) & 0x3F)); + buffer[(*used)++] = (char)(0x80 | (code & 0x3F)); + } else { + buffer[(*used)++] = (char)(0xF0 | (code >> 18)); + buffer[(*used)++] = (char)(0x80 | ((code >> 12) & 0x3F)); + buffer[(*used)++] = (char)(0x80 | ((code >> 6) & 0x3F)); + buffer[(*used)++] = (char)(0x80 | (code & 0x3F)); + } + return true; +} + /* * Строка JSON в память арены. Escape-последовательности разбираются те, что - * есть в самом JSON; `\uXXXX` намеренно НЕ разбирается, и это не лень: ключи и - * значения приезжают из командной строки, где UTF-8 уже написан буквами, а - * половинчатая поддержка суррогатных пар хуже честного отказа. + * есть в самом JSON, и `\uXXXX` тоже — с суррогатными парами (`run_text_unicode`): + * языковой сервер получает JSON от редактора, а клиент вправе экранировать + * любой знак. Одинокая половина пары — отказ, а не подмена знаком. */ static bool run_text(const char *text, size_t *at, char **out, size_t *bytes) { size_t start = 0; @@ -9900,8 +9966,11 @@ static bool run_text(const char *text, size_t *at, char **out, size_t *bytes) { } else if (next == 'r') { buffer[used] = '\r'; } else if (next == 'u') { - free(buffer); - return false; + if (!run_text_unicode(text, &index, *at, buffer, &used)) { + free(buffer); + return false; + } + continue; } else { buffer[used] = next; } @@ -20068,6 +20137,20 @@ static char *lsp_path_of(const char *uri, const char *base) { return result; } +/* + * Сколько уже пришло на стандартный ввод — не больше `size` байт, но и не + * дожидаясь их: `fread` ждёт полный кусок или конец ввода, а редактор ввод не + * закрывает никогда, и ответ на первую же короткую рамку не уходил бы вовсе. + * Ноль — конец ввода, отрицательное — ошибка чтения. + */ +static ssize_t repl_read_some(char *chunk, size_t size) { + ssize_t got = read(STDIN_FILENO, chunk, size); + while (got < 0 && errno == EINTR) { + got = read(STDIN_FILENO, chunk, size); + } + return got; +} + /* ─────────────────────────── рамки Content-Length ───────────────────────── */ /* @@ -20136,11 +20219,11 @@ static char *lsp_read_frame(repl_buf *tail, size_t *bytes) { } { char chunk[8192]; - const size_t got = fread(chunk, 1, sizeof(chunk), stdin); - if (got == 0) { + const ssize_t got = repl_read_some(chunk, sizeof(chunk)); + if (got <= 0) { return NULL; } - buf_add(tail, chunk, got); + buf_add(tail, chunk, (size_t)got); } } } @@ -20571,11 +20654,11 @@ static char *mcp_read_line(repl_buf *tail, size_t *bytes) { } { char chunk[8192]; - const size_t got = fread(chunk, 1, sizeof(chunk), stdin); - if (got == 0) { + const ssize_t got = repl_read_some(chunk, sizeof(chunk)); + if (got <= 0) { return NULL; } - buf_add(tail, chunk, got); + buf_add(tail, chunk, (size_t)got); } } } diff --git a/scripts/seed-fingerprint b/scripts/seed-fingerprint index a3e4ab9f3..9e5482670 100644 --- a/scripts/seed-fingerprint +++ b/scripts/seed-fingerprint @@ -4,7 +4,7 @@ предел-глубины 20000 вход flang/self/bootstrap/compiler.flang ключи --cli --repl -коммит c7226d3954a36d7c4187a1f11a38dd65d94c1771 +коммит 23e32648918d9cbea8c6e4f69c15e1d0431b9640 e200119e04672d9c574cd22cb41f6358a9508a1810f8a4f43ad287a8add8f923 flang/core/evaluate.flang 363ef4fdf66c10a13853083b3aa4aea6b6308c21045171cf5196e02c7871991f flang/core/json.flang a2e66b43e429c19ca3db02e522c07ffdf50fe9d031824693dda75ca511270993 flang/self/bootstrap/compiler.flang @@ -46,7 +46,7 @@ c1773e1fc1dfdfef5fd2a31565603b5bf319a27b40625782719c84b79f5a595a flang/self/pro ce2dc6c1920ee099b4ad91a0f57a8bd1993d832fd80a389b3005fabd022056cd flang/self/totality.flang e22a3744a42a67c1b73fca9fdaa7a45b59578a3f8319f22d56dbc32e2e2602fc flang/self/types.flang a5097ad4267b85fcdadad861e6fae6da05f8bc766979ca6561e3adb27f53d444 flang/src/emit/c/flang_cli.c -d810071f36305e3999a5ceccf61ee56dddfed242fff2487744df2758eca5ada5 flang/src/emit/c/flang_repl.c +93af8470a2658534f0a57fab4b0588e1073bc197bd72f044484a5b5d0f62102b flang/src/emit/c/flang_repl.c b4eac3506b348dcd5cfec6263c52de63a3535db522b78653c5d66eba26c1f975 flang/src/emit/c/flang_runtime.c c6f91919708b33ca4ed2ab4571e055e6dfe9585e62cfab1a266106495f1a2c27 flang/src/emit/c/flang_runtime.h 7bd6e6f32172728767ea9b7a1493638d11c91f349e033769d182fc4d04edbec8 flang/stdlib/hashmap.flang @@ -60,6 +60,6 @@ c6f91919708b33ca4ed2ab4571e055e6dfe9585e62cfab1a266106495f1a2c27 flang/src/emit 0a99c99d62952defe332709df03a70914d4f01cfa76b3d61374176928a3916ac bootstrap/compiler_flang.c 86d44cc5e225bbd2bcd6114b660b19c0ee6538a8148424f344bbd3ebf248e095 bootstrap/compiler_flang.h 000c71e0993a18cf9418355c58fac28704ad3ae6ecff281a5fe3a630a48c09e9 bootstrap/flang_cli.c -d0315e62fb3da46fdf70937029132b708e43294ec1714161832ec03e2b0e7038 bootstrap/flang_repl.c +802625b9a29ca999b78ad38e4a32a4319d38f50a8fa81701f78bc5851a498122 bootstrap/flang_repl.c 8119e748614dd3824880bcbba2183d2ec3b5a5f0b5526f901d0a3cb0e4613c72 bootstrap/flang_runtime.c 622314079f335f38a34c60603072b112d2380ff898c96ce40cfd4a9dc4087bf5 bootstrap/flang_runtime.h