mdview2: горизонтальный скролл по типу блока + фиксы fence/HR/SDCC
Горизонтальный скролл (Фаза 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>
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
# [z80] `global = local` after `if (local != global)` stores `local - global` (A clobbered by comparison)
|
||||
|
||||
## Summary
|
||||
|
||||
On the z80 backend, the sequence
|
||||
|
||||
```c
|
||||
if (n != g) { g = n; }
|
||||
```
|
||||
|
||||
where `n` is in register `A` and `g` is a global, miscompiles. The compiler
|
||||
evaluates the `!=` comparison with `SUB A,(HL)`, which destroys `A`, and then
|
||||
emits the assignment `g = n` by storing `A` — which now holds `n - g`, not `n`.
|
||||
As a result the global ends up holding `(unsigned char)(n - old_g)` instead of `n`.
|
||||
|
||||
It should either use `CP (HL)` (which leaves `A` intact) for the comparison, or
|
||||
reload `n` before the store.
|
||||
|
||||
## Version
|
||||
|
||||
SDCC 4.5.0 #15242 (Mac OS X x86_64). Default options; also reproduces with
|
||||
`--opt-code-speed` and with `--no-peep` (so this is a code-generator bug, not a
|
||||
peephole-optimizer bug).
|
||||
|
||||
## Minimal reproducer
|
||||
|
||||
```c
|
||||
unsigned char vx;
|
||||
|
||||
void update(unsigned char n)
|
||||
{
|
||||
if (n != vx) {
|
||||
vx = n;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Build:
|
||||
|
||||
```
|
||||
sdcc -mz80 -S repro.c
|
||||
```
|
||||
|
||||
## Generated assembly (wrong)
|
||||
|
||||
```asm
|
||||
_update::
|
||||
;repro.c: if (n != vx) {
|
||||
ld hl, #_vx
|
||||
sub a, (hl) ; A (= n) is destroyed: A = n - vx
|
||||
ret Z
|
||||
;repro.c: vx = n;
|
||||
ld (_vx+0), a ; stores (n - vx) instead of n
|
||||
;repro.c: }
|
||||
ret
|
||||
```
|
||||
|
||||
With `--no-peep` the same defect is present (only the branch shape differs):
|
||||
|
||||
```asm
|
||||
_update::
|
||||
ld iy, #_vx
|
||||
sub a, 0 (iy) ; A (= n) destroyed
|
||||
jp NZ, 00112$
|
||||
jp 00103$
|
||||
00112$:
|
||||
ld (_vx+0), a ; stores (n - vx)
|
||||
00103$:
|
||||
ret
|
||||
```
|
||||
|
||||
## Why it happens
|
||||
|
||||
`n` arrives in `A` (sdcccall). The code generator picks `SUB A,(HL)` to evaluate
|
||||
the relational `n != vx`. `SUB` overwrites `A` with the difference. The generator
|
||||
then treats the still-live value `n` as if it were still in `A` and emits a bare
|
||||
store `LD (_vx),A` for the assignment, without reloading `n` first. Because the
|
||||
defect survives `--no-peep`, it is in code generation (register/lifetime tracking
|
||||
across the comparison), not in the peephole optimizer.
|
||||
|
||||
The correct lowering for the comparison is `CP (HL)`, which sets the flags exactly
|
||||
like `SUB` but preserves `A`, so the subsequent store would be correct with no
|
||||
extra instructions.
|
||||
|
||||
## Variants that also reproduce
|
||||
|
||||
- `if (n == vx) return; vx = n;` (early-return form)
|
||||
- `n` coming from a function call result instead of a parameter
|
||||
- both `-mz80` default and `--opt-code-speed`
|
||||
|
||||
## Workaround
|
||||
|
||||
Store into the global *before* the comparison, so the destructive `SUB` is no
|
||||
longer on the store path; compare a saved copy instead:
|
||||
|
||||
```c
|
||||
void update(unsigned char n)
|
||||
{
|
||||
unsigned char old = vx;
|
||||
vx = n; /* store first, A still holds n */
|
||||
if (n != old) {
|
||||
/* side effect */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
generates the correct:
|
||||
|
||||
```asm
|
||||
_update::
|
||||
ld (_vx+0), a
|
||||
ret
|
||||
```
|
||||
|
||||
## Files in this directory
|
||||
|
||||
- `repro.c` — minimal reproducer
|
||||
- `repro.asm` — generated output, default options (defect visible)
|
||||
- `repro.nopeep.asm` — generated output with `--no-peep` (defect still present)
|
||||
- `workaround.c` / `workaround.asm` — store-before-compare workaround (correct)
|
||||
|
||||
## Tracker search
|
||||
|
||||
A search of the SDCC bug tracker did not turn up an exact duplicate. The closest
|
||||
version-matching report, #3834 "[Z80][SDCC 4.5] Compiler bug", is a *different*
|
||||
defect (`genPointerSet`, swapped push/pop order), not this comparison-clobbers-A
|
||||
case.
|
||||
@@ -0,0 +1,127 @@
|
||||
# [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».
|
||||
@@ -0,0 +1,61 @@
|
||||
;--------------------------------------------------------
|
||||
; File Created by SDCC : free open source ISO C Compiler
|
||||
; Version 4.5.0 #15242 (Mac OS X x86_64)
|
||||
;--------------------------------------------------------
|
||||
.module repro
|
||||
|
||||
.optsdcc -mz80 sdcccall(1)
|
||||
;--------------------------------------------------------
|
||||
; Public variables in this module
|
||||
;--------------------------------------------------------
|
||||
.globl _update
|
||||
.globl _vx
|
||||
;--------------------------------------------------------
|
||||
; special function registers
|
||||
;--------------------------------------------------------
|
||||
;--------------------------------------------------------
|
||||
; ram data
|
||||
;--------------------------------------------------------
|
||||
.area _DATA
|
||||
_vx::
|
||||
.ds 1
|
||||
;--------------------------------------------------------
|
||||
; ram data
|
||||
;--------------------------------------------------------
|
||||
.area _INITIALIZED
|
||||
;--------------------------------------------------------
|
||||
; absolute external ram data
|
||||
;--------------------------------------------------------
|
||||
.area _DABS (ABS)
|
||||
;--------------------------------------------------------
|
||||
; global & static initialisations
|
||||
;--------------------------------------------------------
|
||||
.area _HOME
|
||||
.area _GSINIT
|
||||
.area _GSFINAL
|
||||
.area _GSINIT
|
||||
;--------------------------------------------------------
|
||||
; Home
|
||||
;--------------------------------------------------------
|
||||
.area _HOME
|
||||
.area _HOME
|
||||
;--------------------------------------------------------
|
||||
; code
|
||||
;--------------------------------------------------------
|
||||
.area _CODE
|
||||
;repro.c:20: void update(unsigned char n)
|
||||
; ---------------------------------
|
||||
; Function update
|
||||
; ---------------------------------
|
||||
_update::
|
||||
;repro.c:22: if (n != vx) {
|
||||
ld hl, #_vx
|
||||
sub a, (hl)
|
||||
ret Z
|
||||
;repro.c:23: vx = n;
|
||||
ld (_vx+0), a
|
||||
;repro.c:25: }
|
||||
ret
|
||||
.area _CODE
|
||||
.area _INITIALIZER
|
||||
.area _CABS (ABS)
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Minimal reproducer — SDCC z80 codegen bug.
|
||||
*
|
||||
* "global = local" right after "if (local != global)" stores a WRONG value:
|
||||
* the SUB used to evaluate the comparison clobbers register A (which still
|
||||
* holds `n`), and the store reuses that clobbered A.
|
||||
*
|
||||
* Build (stock SDCC, no extra flags, no custom toolchain):
|
||||
* sdcc -mz80 -S repro.c
|
||||
*
|
||||
* Tested with: SDCC 4.5.0 #15242 (Mac OS X x86_64), default options.
|
||||
* Also reproduces with --opt-code-speed and with --no-peep
|
||||
* (so it is a code-generator bug, not a peephole-optimizer bug).
|
||||
*
|
||||
* Expected semantics: after update(n), vx == n whenever n != vx.
|
||||
* Actual result: vx == (unsigned char)(n - old_vx).
|
||||
*/
|
||||
unsigned char vx;
|
||||
|
||||
void update(unsigned char n)
|
||||
{
|
||||
if (n != vx) {
|
||||
vx = n;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
;--------------------------------------------------------
|
||||
; File Created by SDCC : free open source ISO C Compiler
|
||||
; Version 4.5.0 #15242 (Mac OS X x86_64)
|
||||
;--------------------------------------------------------
|
||||
.module repro
|
||||
|
||||
.optsdcc -mz80 sdcccall(1)
|
||||
;--------------------------------------------------------
|
||||
; Public variables in this module
|
||||
;--------------------------------------------------------
|
||||
.globl _update
|
||||
.globl _vx
|
||||
;--------------------------------------------------------
|
||||
; special function registers
|
||||
;--------------------------------------------------------
|
||||
;--------------------------------------------------------
|
||||
; ram data
|
||||
;--------------------------------------------------------
|
||||
.area _DATA
|
||||
_vx::
|
||||
.ds 1
|
||||
;--------------------------------------------------------
|
||||
; ram data
|
||||
;--------------------------------------------------------
|
||||
.area _INITIALIZED
|
||||
;--------------------------------------------------------
|
||||
; absolute external ram data
|
||||
;--------------------------------------------------------
|
||||
.area _DABS (ABS)
|
||||
;--------------------------------------------------------
|
||||
; global & static initialisations
|
||||
;--------------------------------------------------------
|
||||
.area _HOME
|
||||
.area _GSINIT
|
||||
.area _GSFINAL
|
||||
.area _GSINIT
|
||||
;--------------------------------------------------------
|
||||
; Home
|
||||
;--------------------------------------------------------
|
||||
.area _HOME
|
||||
.area _HOME
|
||||
;--------------------------------------------------------
|
||||
; code
|
||||
;--------------------------------------------------------
|
||||
.area _CODE
|
||||
;repro.c:20: void update(unsigned char n)
|
||||
; ---------------------------------
|
||||
; Function update
|
||||
; ---------------------------------
|
||||
_update::
|
||||
;repro.c:22: if (n != vx) {
|
||||
ld iy, #_vx
|
||||
sub a, 0 (iy)
|
||||
jp NZ, 00112$
|
||||
jp 00103$
|
||||
00112$:
|
||||
;repro.c:23: vx = n;
|
||||
ld (_vx+0), a
|
||||
00103$:
|
||||
;repro.c:25: }
|
||||
ret
|
||||
.area _CODE
|
||||
.area _INITIALIZER
|
||||
.area _CABS (ABS)
|
||||
@@ -0,0 +1,57 @@
|
||||
;--------------------------------------------------------
|
||||
; File Created by SDCC : free open source ISO C Compiler
|
||||
; Version 4.5.0 #15242 (Mac OS X x86_64)
|
||||
;--------------------------------------------------------
|
||||
.module workaround
|
||||
|
||||
.optsdcc -mz80 sdcccall(1)
|
||||
;--------------------------------------------------------
|
||||
; Public variables in this module
|
||||
;--------------------------------------------------------
|
||||
.globl _update
|
||||
.globl _vx
|
||||
;--------------------------------------------------------
|
||||
; special function registers
|
||||
;--------------------------------------------------------
|
||||
;--------------------------------------------------------
|
||||
; ram data
|
||||
;--------------------------------------------------------
|
||||
.area _DATA
|
||||
_vx::
|
||||
.ds 1
|
||||
;--------------------------------------------------------
|
||||
; ram data
|
||||
;--------------------------------------------------------
|
||||
.area _INITIALIZED
|
||||
;--------------------------------------------------------
|
||||
; absolute external ram data
|
||||
;--------------------------------------------------------
|
||||
.area _DABS (ABS)
|
||||
;--------------------------------------------------------
|
||||
; global & static initialisations
|
||||
;--------------------------------------------------------
|
||||
.area _HOME
|
||||
.area _GSINIT
|
||||
.area _GSFINAL
|
||||
.area _GSINIT
|
||||
;--------------------------------------------------------
|
||||
; Home
|
||||
;--------------------------------------------------------
|
||||
.area _HOME
|
||||
.area _HOME
|
||||
;--------------------------------------------------------
|
||||
; code
|
||||
;--------------------------------------------------------
|
||||
.area _CODE
|
||||
;workaround.c:4: void update(unsigned char n)
|
||||
; ---------------------------------
|
||||
; Function update
|
||||
; ---------------------------------
|
||||
_update::
|
||||
ld (_vx+0), a
|
||||
;workaround.c:8: if (n != old) {
|
||||
;workaround.c:11: }
|
||||
ret
|
||||
.area _CODE
|
||||
.area _INITIALIZER
|
||||
.area _CABS (ABS)
|
||||
@@ -0,0 +1,11 @@
|
||||
/* Workaround: store BEFORE the comparison so the SUB that evaluates the
|
||||
* comparison no longer sits on the store path. Generates correct code. */
|
||||
unsigned char vx;
|
||||
void update(unsigned char n)
|
||||
{
|
||||
unsigned char old = vx;
|
||||
vx = n; /* store first, with A still holding n */
|
||||
if (n != old) {
|
||||
/* redraw / side effect */
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user