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
3.2 KiB
Markdown
128 lines
3.2 KiB
Markdown
# [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.
|