diff --git a/Makefile b/Makefile index a3888d1..30e4c65 100644 --- a/Makefile +++ b/Makefile @@ -13,13 +13,15 @@ # Most heavy lifting is delegated to sub-Makefiles. # Small libc-feature tests (one program per .c-language feature or libc API). -TESTS := hello banked bankedbg strtest cat seek malloc mem_test argv errno \ - rt_test openenv ls conio attrprob timedir mouse banklocl stdlib \ - assrtest ptime stattest filetest fdmax fbench solidt \ - gfx_demo gfx_d16 gfx_text gfx_mous +TESTS := hello hello2 simple banked bankedbg banktest strtest cat seek \ + malloc mem_test argv errno rt_test openenv ls conio conio2 \ + attrprob timedir mouse banklocl stdlib assrtest ptime stattest \ + filetest fdmax fbench solidt dec_test gets stest2 winrest \ + bios_text text_palette \ + gfx_demo gfx_d16 gfx_text gfx_mous gfx_dbuf # Larger end-user applications under examples/. -APPS := mdview +APPS := mdview mdview2 MAME_DIR := mame/v306 FLOPPY_IMG := $(MAME_DIR)/IMG/mc.img @@ -34,7 +36,8 @@ DATA_FILES := \ tests/seek/big.txt \ examples/mdview/SAMPLE.MD -.PHONY: all tools lib tests examples check clean sdcc floppy $(TESTS) $(APPS) +.PHONY: all tools lib tests examples check clean sdcc floppy \ + size-check size-baseline $(TESTS) $(APPS) all: tools lib tests examples @@ -67,6 +70,14 @@ floppy: tests examples tests/seek/big.txt @echo "Floppy ready: $(FLOPPY_IMG)" @echo "Run: cd $(MAME_DIR) && ./run_mame.sh" +# Размерный регресс: сверить _CODE всех программ с docs/size_baseline.tsv. +size-check: + python3 toolchain/size_check.py + +# Принять текущие размеры как эталон (после осознанных изменений). +size-baseline: + python3 toolchain/size_check.py --update + clean: $(MAKE) -C toolchain/mkexe clean $(MAKE) -C lib clean diff --git a/docs/libc-headers.md b/docs/libc-headers.md new file mode 100644 index 0000000..49ccf2b --- /dev/null +++ b/docs/libc-headers.md @@ -0,0 +1,46 @@ +# Заголовки libc: контракт затенения SDCC (2026-07-06) + +`libc/include` стоит в -I ПЕРЕД заголовками SDCC, поэтому наш файл с +именем стандартного заголовка «затеняет» SDCC-шный. Два разрешённых +паттерна: + +## 1. Цепочка `#include_next` — только ДОБАВЛЯЕМ + +Наш заголовок первым делом делает `#include_next <имя>` (берёт +SDCC-версию) и дальше только добавляет Sprinter-расширения. Ничего +из стандартной части не переобъявлять — malloc/strlen/… должны +приходить из SDCC, иначе разъедутся прототипы с z80.lib (уже кусало: +полный shadow stdlib.h терял malloc/free). + +| Заголовок | Что добавляем | +|---|---| +| `stdlib.h` | min/max (функции, int16_t — как `int min()` в Solid-C) | +| `string.h` | strlwr/strupr (CP866-регистры) | + +## 2. Полная замена — обязаны продублировать контракт SDCC + +Наш заголовок полностью замещает SDCC-шный. Он ОБЯЗАН объявить всё, +что программы берут из z80.lib, с точными SDCC-сигнатурами: + +| Заголовок | Обязан объявлять (из z80.lib) | Наше | +|---|---|---| +| `stdio.h` | printf, sprintf, vprintf, vsprintf | FILE* API (буферизованный B+), puts/putchar/getchar (наши, ESTEX), scanf-семейство, dec*/hex*, gets | +| `time.h` | struct tm, time_t, time, mktime, gmtime, localtime, asctime, ctime — **раскладка struct tm и __TIME_UNSIGNED=1 должны совпадать с SDCC ABI** (см. шапку time.h) | datetime_t, getdatetime/setdatetime, DOW_* | + +При апгрейде SDCC сверять сигнатуры этих двух заголовков с +`third_party/sdcc/share/sdcc/include/`. + +## 3. Свои заголовки (SDCC-аналога нет — затенения нет) + +conio.h, dir.h, dos.h, errno.h, fcntl.h, gfx.h, mouse.h, palette.h, +sprinter*.h, unistd.h, bios/*. + +## Правила + +- новый стандартный заголовок — сначала пробовать паттерн 1 + (include_next); паттерн 2 — только если надо переопределить + реализацию (как puts/putchar на ESTEX); +- в заголовках паттерна 2 — комментарий, какие декларации обслуживают + z80.lib; +- internal-заголовки libc (`_conio.h`, `_file.h`, `_gfx.h`, …) живут + РЯДОМ с исходниками в libc//, не в libc/include. diff --git a/docs/libc-roadmap.md b/docs/libc-roadmap.md index 8dc1243..5e23b2a 100644 --- a/docs/libc-roadmap.md +++ b/docs/libc-roadmap.md @@ -68,21 +68,21 @@ fgetpos/fsetpos (хвост П2). bdos/brk/ioctl — отказ решение - [x] rename() — ESTEX RENAME $10 (HL=старое, DE=новое), libc/io/rename.c - [x] isatty(fd) — fd < 2 (манипуляторы DSS с 2; консольные псевдо-fd 0/-1/-2) -## П5. Заголовки и гигиена сборки +## П5. Заголовки и гигиена сборки — ЗАКРЫТ 2026-07-06 -- [ ] Аудит затенения SDCC-заголовков: stdlib.h уже на `#include_next`; - stdio.h/time.h — полные замены (задокументировать контракт «что обязаны - объявлять»); подумать про собственный string.h (include_next + strlwr/ - strupr из sprinter_compat.h) -- [ ] lib/build: wildcard не удаляет stale .rel при удалении исходника — - либо чистка в Makefile (сверка списка), либо привычка `make clean` - после переименований (это уже укусило: старый file.rel лежал в build) -- [ ] Включить extra-тесты (conio2, dec_test, gets, hello2, simple, stest2, - winrest, bios_text, text_palette, gfx_dbuf, mdview2) в top-level - TESTS/APPS — иначе `make all` их не собирает и их baseline протухает - (уже дало ложные «+17 Б» при сравнении) -- [ ] Размерный регресс-тест: скрипт сравнения _CODE по .map с эталоном - (ловить случайные разжирения при правках libc) +- [x] Контракт затенения — **docs/libc-headers.md**: include_next + (stdlib.h + новый string.h со strlwr/strupr) vs полная замена + (stdio.h, time.h — обязаны дублировать сигнатуры z80.lib); + из sprinter_compat.h убраны макросы min/max (конфликтовали с + функциями из stdlib.h; в Solid-C это тоже функции) +- [x] lib/Makefile: stale .rel чистятся сверкой списка перед упаковкой; + штамп .modules триггерит перелинковку при смене состава (и сносит + архив — mtime на exFAT грубый) +- [x] Все extra-тесты в top-level TESTS (43 программы: + hello2, simple, + banktest (переименован из banked.exe), conio2, dec_test, gets, + stest2, winrest, bios_text, text_palette, gfx_dbuf) и mdview2 в APPS +- [x] Размерный регресс: toolchain/size_check.py + docs/size_baseline.tsv; + `make size-check` (выход 1 при росте) / `make size-baseline` ## П6. Верификация после сплита diff --git a/docs/size_baseline.tsv b/docs/size_baseline.tsv new file mode 100644 index 0000000..cbb9018 --- /dev/null +++ b/docs/size_baseline.tsv @@ -0,0 +1,44 @@ +# Эталон размеров _CODE (байт); обновление: python3 toolchain/size_check.py --update +argv 3431 +assrtest 3847 +attrprob 4091 +banked 1040 +bankedbg 1051 +banklocl 4816 +banktest 3751 +bios_text 4470 +cat 915 +conio 4634 +conio2 3946 +dec_test 860 +errno 6216 +fbench 9040 +fdmax 6337 +filetest 11624 +gets 509 +gfx_d16 3319 +gfx_dbuf 2396 +gfx_demo 3219 +gfx_mous 5542 +gfx_text 2560 +hello 4176 +hello2 4251 +ls 4919 +malloc 4462 +mdview 19122 +mdview2 27029 +mem_test 4558 +mouse 4394 +openenv 6111 +ptime 5896 +rt_test 4103 +seek 4177 +simple 963 +solidt 12381 +stattest 7700 +stdlib 6643 +stest2 3644 +strtest 1340 +text_palette 5039 +timedir 5471 +winrest 4481 diff --git a/lib/Makefile b/lib/Makefile index dd4ae68..a77344d 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -36,6 +36,16 @@ RUNTIME_RELS := $(patsubst runtime/%.s,$(BUILD)/%.rel,$(RUNTIME_S)) ALL_RELS := $(LIBC_RELS) $(RUNTIME_RELS) +# Штамп списка модулей: обновляется при любом изменении состава +# (добавление/удаление/переименование исходника) — иначе удаление +# исходника не триггерит перелинковку и stale .rel остаётся в архиве. +# Сразу сносим и архив: на exFAT (SAM8) гранулярность mtime грубая, +# и «список изменился» через сравнение времён можно проглядеть. +MODLIST := $(BUILD)/.modules +ifneq ($(shell cat $(MODLIST) 2>/dev/null),$(ALL_RELS)) +_ := $(shell mkdir -p $(BUILD); echo "$(ALL_RELS)" > $(MODLIST); rm -f sprinter.lib) +endif + LIB := sprinter.lib all: $(LIB) @@ -52,7 +62,16 @@ $(BUILD)/%.rel: $(PROJ_ROOT)/runtime/%.s $(SDASZ80) -o $@ $< # Archive — sdar with `rcs` = replace/create/symtab. -$(LIB): $(ALL_RELS) +# Перед упаковкой удаляем stale .rel: wildcard не знает об удалённых/ +# переименованных исходниках, и осиротевший .rel из build/ иначе жил бы +# вечно (уже кусало: старый file.rel после сплита). +$(LIB): $(ALL_RELS) $(MODLIST) + @find $(BUILD) -name '*.rel' -print | sort > $(BUILD)/.have + @printf '%s\n' $(ALL_RELS) | sort > $(BUILD)/.want + @comm -23 $(BUILD)/.have $(BUILD)/.want | while read f; do \ + echo " stale: $$f (исходник удалён) — rm"; \ + rm -f "$$f" "$${f%.rel}.asm" "$${f%.rel}.lst" "$${f%.rel}.sym"; \ + done rm -f $@ $(SDAR) -rcs $@ $(ALL_RELS) @echo diff --git a/libc/include/sprinter_compat.h b/libc/include/sprinter_compat.h index e0c88af..f975886 100644 --- a/libc/include/sprinter_compat.h +++ b/libc/include/sprinter_compat.h @@ -71,9 +71,7 @@ typedef struct fpoint { /* movmem(src, dst, n) → memcpy(dst, src, n) — arg order swapped */ #define movmem(s, d, n) memcpy((d), (s), (n)) -/* In-place case conversion. */ -char *strlwr(char *s); -char *strupr(char *s); +/* strlwr/strupr объявлены в нашем (include_next-цепочка). */ /* Solid-C calls perror's table 'strerr'. */ #define strerr(n) strerror((n)) @@ -81,12 +79,10 @@ char *strupr(char *s); /* ---- stdlib.h additions ------------------------------------------ */ #define abort() _exit(0xFF) -#ifndef min -#define min(a, b) ((a) < (b) ? (a) : (b)) -#endif -#ifndef max -#define max(a, b) ((a) > (b) ? (a) : (b)) -#endif +/* min/max — ФУНКЦИИ из (libc/stdlib/min.c, max.c), как и в + * Solid-C (STDLIB.H: `int min();`). Макро-версии здесь были удалены + * 2026-07-06: макрос дважды вычисляет аргументы и ломает объявление + * функции, встреченное после себя. */ /* div() and div_t come from SDCC's (already included above). */ diff --git a/libc/include/string.h b/libc/include/string.h new file mode 100644 index 0000000..15136ff --- /dev/null +++ b/libc/include/string.h @@ -0,0 +1,18 @@ +/* + * string.h — цепочка к SDCC-шному string.h + Sprinter-расширения. + * + * Вся стандартная строковая библиотека (memcpy/memset/strlen/strcmp/ + * strtok/…) приходит из SDCC z80.lib через include_next — здесь только + * ДОБАВЛЯЕМ, ничего не затеняя (контракт: docs/libc-headers.md). + */ + +#ifndef STRING_H_SPRINTER +#define STRING_H_SPRINTER + +#include_next + +/* Регистры in-place с поддержкой кириллицы CP866 (возвращают s). */ +char *strlwr(char *s); +char *strupr(char *s); + +#endif diff --git a/tests/banktest/Makefile b/tests/banktest/Makefile index c19eca1..e5ed067 100644 --- a/tests/banktest/Makefile +++ b/tests/banktest/Makefile @@ -1,11 +1,11 @@ -# Build banked.exe — HUGE memory mode (small + banked code in W3). +# Build banktest.exe — HUGE memory mode (small + banked code in W3). # # Layout: # CODE/HOME at 0x4100 (W1), DATA at 0x8000 (W2), banks at 0x{N}C000 (W3). # crt0_banked.s loads N banks from the .EXE before calling main. PROJ_ROOT := $(abspath $(CURDIR)/../..) -EXAMPLE := banked +EXAMPLE := banktest MEMORY := huge EXTRA_FLAGS := --bank 1=bank1.c --bank 2=bank2.c include $(PROJ_ROOT)/app.mk diff --git a/tests/banktest/banked.c b/tests/banktest/banktest.c similarity index 100% rename from tests/banktest/banked.c rename to tests/banktest/banktest.c diff --git a/toolchain/size_check.py b/toolchain/size_check.py new file mode 100644 index 0000000..9b9cee7 --- /dev/null +++ b/toolchain/size_check.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +""" +size_check.py — размерный регресс-тест: сравнение _CODE всех тестов и +приложений с эталоном docs/size_baseline.tsv. + +Использование: + python3 toolchain/size_check.py # сверка с эталоном + python3 toolchain/size_check.py --update # перезаписать эталон + +Логика: собираем _CODE из .map каждой программы (tests/*, examples/*), +сравниваем с эталоном. Любое отличие печатается; выход 1 — если есть +РОСТ (случайное разжирение libc), 0 — если только уменьшения/без +изменений. Новые программы (нет в эталоне) — предупреждение, добавить +через --update. Запускать после `make all`. +""" + +import glob +import os +import re +import sys + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BASELINE = os.path.join(ROOT, "docs", "size_baseline.tsv") + + +def collect(): + """Имя программы → размер _CODE (байт) по свежим .map.""" + sizes = {} + pats = [ + os.path.join(ROOT, "tests", "*", ".sprinter-cc-*", "*.map"), + os.path.join(ROOT, "examples", "*", ".sprinter-cc-*", "*.map"), + ] + for pat in pats: + for map_path in glob.glob(pat): + name = os.path.splitext(os.path.basename(map_path))[0] + # .map должен лежать в своей папке .sprinter-cc- + if os.path.basename(os.path.dirname(map_path)) != ".sprinter-cc-" + name: + continue + with open(map_path, encoding="utf-8", errors="replace") as f: + for line in f: + m = re.match(r"^_CODE\s+([0-9A-Fa-f]+)\s+([0-9A-Fa-f]+)", line) + if m: + sizes[name] = int(m.group(2), 16) + break + return sizes + + +def load_baseline(): + base = {} + if os.path.exists(BASELINE): + with open(BASELINE) as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + name, size = line.split("\t") + base[name] = int(size) + return base + + +def save_baseline(sizes): + with open(BASELINE, "w") as f: + f.write("# Эталон размеров _CODE (байт); обновление: " + "python3 toolchain/size_check.py --update\n") + for name in sorted(sizes): + f.write(f"{name}\t{sizes[name]}\n") + + +def main(): + update = "--update" in sys.argv + sizes = collect() + if not sizes: + print("size_check: .map не найдены — сначала `make all`") + return 2 + + if update: + save_baseline(sizes) + print(f"size_check: эталон обновлён ({len(sizes)} программ) → " + f"{os.path.relpath(BASELINE, ROOT)}") + return 0 + + base = load_baseline() + if not base: + print("size_check: эталона нет — создать: size_check.py --update") + return 2 + + grew = 0 + for name in sorted(set(sizes) | set(base)): + new, old = sizes.get(name), base.get(name) + if new is None: + print(f" {name}: исчез из сборки (был {old} Б)") + elif old is None: + print(f" {name}: НОВЫЙ ({new} Б) — добавить в эталон (--update)") + elif new != old: + mark = "РОСТ" if new > old else "ок, меньше" + print(f" {name}: {old} -> {new} ({new - old:+d} Б) [{mark}]") + if new > old: + grew += 1 + if grew: + print(f"size_check: {grew} программ(ы) ВЫРОСЛИ — проверить правки libc") + return 1 + print(f"size_check: OK ({len(sizes)} программ, роста нет)") + return 0 + + +if __name__ == "__main__": + sys.exit(main())