138e783e72
Горизонтальный скролл (Фаза 5, финал): - Скроллим по ТИПУ, а не по длине: новый флаг IF_HSCROLL ставится только на код и таблицы; HR и границы fence (IF_NOWRAP без IF_HSCROLL) не двигаются. Блок едет целиком, включая строки короче 80. - Обход бага кодогенерации SDCC z80: `if (n!=g) g=n;` пишет (n-g) вместо n (SUB сравнения затирает A, store переиспользует испорченный A). Лечится записью viewport_x ДО сравнения. Минимальный репродьюсер и оба описания для трекера — в docs/bugs/sdcc-z80-cmp-store-a/ (воспроизводится на чистом sdcc 4.5, в т.ч. с --no-peep → это кодогенератор, не peephole). Рендеринг: - Отступленный fence (```c внутри списка) теперь распознаётся: is_fence_raw пропускает ведущие пробелы/табы; то же в рендере прячет строку-границу. - Строки-разделители (HR, ровно 80) больше не участвуют в скролле. Чистка: удалён мёртвый код (is_fence_delim, get_init_style[_raw], is_cont, seg_flags). Makefile (mdview/mdview2): iconv UTF-8→CP866 завершается ненулевым кодом при отбрасывании символов — игнорируем (|| true). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
128 lines
4.7 KiB
Markdown
128 lines
4.7 KiB
Markdown
# [z80] `global = local` после `if (local != global)` пишет `local - global` (A затирается сравнением)
|
||
|
||
## Кратко
|
||
|
||
На бэкенде z80 последовательность
|
||
|
||
```c
|
||
if (n != g) { g = n; }
|
||
```
|
||
|
||
где `n` лежит в регистре `A`, а `g` — глобальная переменная, компилируется
|
||
неверно. Сравнение `!=` вычисляется через `SUB A,(HL)`, который **разрушает `A`**,
|
||
после чего присваивание `g = n` выполняется записью `A` — а там уже `n - g`, а не
|
||
`n`. В итоге в глобальную переменную попадает `(unsigned char)(n - старое_g)`
|
||
вместо `n`.
|
||
|
||
Корректно было бы использовать для сравнения `CP (HL)` (он не меняет `A`) либо
|
||
перезагрузить `n` перед записью.
|
||
|
||
## Версия
|
||
|
||
SDCC 4.5.0 #15242 (Mac OS X x86_64). Опции по умолчанию; также воспроизводится с
|
||
`--opt-code-speed` и с `--no-peep` (то есть это баг кодогенератора, а не
|
||
peephole-оптимизатора).
|
||
|
||
## Минимальный пример
|
||
|
||
```c
|
||
unsigned char vx;
|
||
|
||
void update(unsigned char n)
|
||
{
|
||
if (n != vx) {
|
||
vx = n;
|
||
}
|
||
}
|
||
```
|
||
|
||
Сборка:
|
||
|
||
```
|
||
sdcc -mz80 -S repro.c
|
||
```
|
||
|
||
## Сгенерированный ассемблер (неверный)
|
||
|
||
```asm
|
||
_update::
|
||
;repro.c: if (n != vx) {
|
||
ld hl, #_vx
|
||
sub a, (hl) ; A (= n) разрушается: A = n - vx
|
||
ret Z
|
||
;repro.c: vx = n;
|
||
ld (_vx+0), a ; пишет (n - vx) вместо n
|
||
;repro.c: }
|
||
ret
|
||
```
|
||
|
||
С `--no-peep` дефект тот же (отличается лишь форма ветвления):
|
||
|
||
```asm
|
||
_update::
|
||
ld iy, #_vx
|
||
sub a, 0 (iy) ; A (= n) разрушен
|
||
jp NZ, 00112$
|
||
jp 00103$
|
||
00112$:
|
||
ld (_vx+0), a ; пишет (n - vx)
|
||
00103$:
|
||
ret
|
||
```
|
||
|
||
## Почему так происходит
|
||
|
||
`n` приходит в `A` (sdcccall). Кодогенератор выбирает `SUB A,(HL)` для вычисления
|
||
отношения `n != vx`. `SUB` перезаписывает `A` разностью. Далее генератор считает,
|
||
что ещё «живое» значение `n` по-прежнему в `A`, и для присваивания выдаёт голую
|
||
запись `LD (_vx),A`, не перезагрузив `n`. Поскольку дефект сохраняется при
|
||
`--no-peep`, он находится в кодогенерации (учёт регистров/времён жизни через
|
||
сравнение), а не в peephole-оптимизаторе.
|
||
|
||
Правильное преобразование сравнения — `CP (HL)`: он выставляет флаги ровно как
|
||
`SUB`, но сохраняет `A`, поэтому последующая запись была бы корректной без единой
|
||
лишней инструкции.
|
||
|
||
## Варианты, которые тоже воспроизводят
|
||
|
||
- `if (n == vx) return; vx = n;` (форма с ранним выходом)
|
||
- `n` как результат вызова функции, а не как параметр
|
||
- и `-mz80` по умолчанию, и `--opt-code-speed`
|
||
|
||
## Обходной путь (workaround)
|
||
|
||
Записывать в глобальную переменную **до** сравнения, чтобы разрушающий `SUB` не
|
||
оказался на пути записи; сравнивать сохранённую копию:
|
||
|
||
```c
|
||
void update(unsigned char n)
|
||
{
|
||
unsigned char old = vx;
|
||
vx = n; /* запись первой, A ещё держит n */
|
||
if (n != old) {
|
||
/* побочный эффект */
|
||
}
|
||
}
|
||
```
|
||
|
||
даёт корректное:
|
||
|
||
```asm
|
||
_update::
|
||
ld (_vx+0), a
|
||
ret
|
||
```
|
||
|
||
## Файлы в этом каталоге
|
||
|
||
- `repro.c` — минимальный пример
|
||
- `repro.asm` — вывод с опциями по умолчанию (дефект виден)
|
||
- `repro.nopeep.asm` — вывод с `--no-peep` (дефект сохраняется)
|
||
- `workaround.c` / `workaround.asm` — обход «запись до сравнения» (корректно)
|
||
|
||
## Поиск по трекеру
|
||
|
||
Поиск по баг-трекеру SDCC точного дубликата не нашёл. Ближайший по версии отчёт
|
||
#3834 «[Z80][SDCC 4.5] Compiler bug» — это **другой** дефект (`genPointerSet`,
|
||
переставленный порядок push/pop), а не данный случай «сравнение затирает A».
|