Атрибуты, макросы и статический аллокатор в Retrolang

Retrolang похож на C, но сходство лишь базовое. В действительности я не собираюсь копировать C — не только потому что это фактически мне не нужно, но и потому что программирование под PSX требует весьма специфических возможностей языка, почти на уровне DSL (при этом я не стал их прибивать гвоздями к MIPS, они теоретически могут быть реализованы на системном уровне для любой архитектуры).

Во-первых, это, конечно, атрибуты размещения и инициализации статических данных. Я почти сразу сделал поддержку вшивания в бинарник файлов — это очень полезно, чтобы писать мини-игры, которым не требуется читать файлы с диска, а также чтобы встроить в программу bitmap-шрифт или какие-то другие данные.

uchar data[] @("file.bin");

На PSX слишком мало памяти, чтобы использовать динамический аллокатор, как в современных ОС. Программа должна заранее распланировать память, которую она потребляет. Если доверить эту задачу компилятору, то он может все проверять и не допускать коллизий (в то время как ручная работа с диапазонами адресов чревата ошибками). Для этого в Retrolang используются все те же атрибуты:

macro Region = (0x80050000, 2048);
int someData[512] @Region;

Это статически выделит 512*4 байт в регионе MyRegion. Для этого в кодогенераторе предусмотрена специальная таблица регионов и аллокаций.

Преимущество такого подхода перед выделением статической памяти в секции данных в том, что бинарник остается маленьким, а это иногда критично. Кроме того, регионы поддерживают Scratchpad:

int someData[64] @SCRATCHPAD;

Выделенную память можно затем использовать в качестве арены для создания в рантайме более мелких объектов.

Во-вторых, AST-макросы. Это киллер-фича, которая делает ненужным препроцессор C. Препроцессор в Retrolang все равно есть, но возможности его ограничены, и я не планирую их развивать. Другое дело — макросы. Они позволяют переиспользовать практически любые выражения и утверждения языка, параметризуя их. Это похоже не шаблоны, но шаблон обычно параметризуется типами, а макрос — константами, идентификаторами, выражениями и другими макросами. Как и шаблоны, макросы — тоже разновидность метапрограммирования, но их главное назначение — явный инлайнинг кода. В условиях, когда вызов лишней процедуры — дорогое удовольствие, язык должен позволять переиспользовать код статически, во время компиляции. Например, вместо того, чтобы создавать функцию с командами GTE, можно сделать соответствующий макрос, и команды будут вставлены непосредственно в вызывающий код без оверхеда на движение по стеку:

macro gte_upload_transform(tr)
{
    gte_ctc2(GTE_TRX, tr->tx);
    gte_ctc2(GTE_TRY, tr->ty);
    gte_ctc2(GTE_TRZ, tr->tz);
    
    gte_set_matrix(tr->r);
    
    gte_ctc2(GTE_H,   tr->h);
    gte_ctc2(GTE_OFX, tr->ofx);
    gte_ctc2(GTE_OFY, tr->ofy);
    gte_ctc2(GTE_DQA, tr->dqa);
    gte_ctc2(GTE_DQB, tr->dqb);
}

Макросы также удобны для создания удобочитаемых оберток над функциями BIOS.