mcpu-cpp – препроцессор языков программирования MCPU. Он является самостоятельным компонентом экосистемы LibMPU/LibMPUIO/LibMCPU и не привязан к названию одного конкретного языка: активный язык выбирается директивой #lang.

Этот документ задаёт нормативное поведение mcpu-cpp: текстовую модель, директивы, macro engine, include pipeline, конфигурацию, диагностику и генерацию зависимостей для инструментов MCPU.

1. Текстовая модель

Внешние исходные файлы и конфигурационные файлы имеют кодировку UTF-8. UTF-8 должен быть корректным. Для исходных программ проверка принадлежности символов диапазону UCS-2 выполняется после удаления комментариев: поэтому корректный Unicode scalar value выше U+FFFF допустим внутри комментария, но остаётся ошибкой в программном тексте. После этой стадии исходный текст обрабатывается как последовательность __mpu_char16_t. Входной UTF-8 BOM допускается и удаляется. Встроенный NUL в исходном файле запрещён.

Переводы строк CRLF и CR нормализуются в LF.

2. Действия, выполняемые независимо от директив

mcpu-cpp выполняет несколько преобразований до разбора директив.

2.1. Backslash-newline

Последовательность \\ непосредственно перед переводом строки удаляется до распознавания комментариев, директив и макросов. Поэтому, например,

#defi\
ne FOO 10\
20

эквивалентно логической строке

#define FOO 1020

При этом физические номера строк продолжают учитываться при формировании текущей позиции; если пользователь не менял её директивой #line, они и будут видны в генерируемых line marker-ах.

2.2. Комментарии

Комментарии /* ... */ и // ... удаляются до последующей обработки. Там, где это необходимо для разделения соседних токенов, сохраняется пробельный разделитель. Если комментарий завершает непустую строку, после его удаления не сохраняются ни синтетический разделитель, ни пробелы, предшествовавшие комментарию: строка заканчивается последним значащим символом. Это относится и к многострочному комментарию, начавшемуся после программного текста. Если после удаления комментария строка вообще не содержит ничего кроме пробелов, она становится действительно пустой строкой. При этом комментарий между двумя токенами по-прежнему оставляет необходимый разделитель и не склеивает их. Переводы строк сохраняются, чтобы не разрушать координаты исходного текста.

Комментарий не распознаётся внутри строковой или символьной константы. Для языка diff апостроф не считается началом символьной константы, поскольку используется в обозначениях производных.

В буквальном аргументе #include <...> последовательности /* и // рассматриваются как часть имени файла.

3. Директивы и выходной поток

Директива начинается символом #, если до него в логической строке находятся только пробельные символы или комментарии. Между # и именем директивы допускаются пробелы.

Служебная информация о позиции в выходном потоке представлена в форме GNU line marker:

# номер "имя-файла" [флаги]

Это не входная директива #line. При входе во включаемый файл к line marker-у добавляется флаг 1, а при возврате в файл, содержащий #include, – флаг 2. Эти значения имеют тот же смысл, что и в GNU CPP: 1 означает вход в новый файл, 2 – возврат в предыдущий файл. Флаг 2 не является числом или уровнем вложенности.

Например:

# 1 "main.c"
# 1 "defs.h" 1
...
# 2 "main.c" 2

Входная директива

#line 62 "main.y"

сама в выходной поток не копируется. Она изменяет логические значения __LINE__ и __FILE__ для последующего текста, а в выходе представляется line marker-ом:

# 62 "main.y"

Аргументы #line предварительно подвергаются macro expansion, как в принятой модели line control. Если после такого #line происходит #include, то после возврата marker получает флаг 2, например # 65 "main.y" 2. Имя, заданное через #line, становится логическим именем для __FILE__ и line marker-ов; оно не меняет каталог, относительно которого ищется quoted #include.

Директивы препроцессора имеют только канонические английские имена. Unicode остаётся полностью допустимым в идентификаторах, строках, комментариях и другом пользовательском тексте.

4. Заголовочные файлы

Поддерживаются:

#include "file"
#include <file>
#include_next "file"
#include_next <file>
#pragma once

Для обычного #include "file" первым всегда проверяется каталог физического текущего исходного файла. Логическое имя, установленное через #line, на этот шаг не влияет. Для #include <file> каталог текущего файла не проверяется.

4.1. Перемещаемый корень MCPU как общий принцип экосистемы

Начиная с выпуска 0.0.37 каталог установки MCPU не содержит версию конкретного инструмента и не является абсолютной runtime-константой, зашитой в бинарный файл. Версия относится к самому mcpu-cpp, mcpu-as, mcpu-ld, mcpu-run или библиотеке, но не определяет корень единой среды MCPU.

При типичной конфигурации:

./configure --prefix=/usr --libdir=/usr/lib64

make install создаёт дерево:

/usr/lib64/mcpu/
├── bin/
│   └── mcpu-cpp
├── etc/
│   └── mcpu-cpp.conf
├── include/
│   ├── diff/
│   ├── dift/
│   ├── alg/
│   ├── as/
│   ├── avm/
│   └── acs/
└── lib/                  # общий каталог будущих библиотек MCPU

Публичное имя программы находится в $bindir:

/usr/bin/mcpu-cpp -> ../lib64/mcpu/bin/mcpu-cpp

Абсолютный /usr/lib64/mcpu при этом не является частью runtime ABI MCPU-CPP. Он используется только make install как выбранное configure-time место размещения файлов.

При каждом обычном запуске MCPU-CPP определяет фактический путь собственного исполняемого файла через Linux /proc/self/exe. Символическая ссылка публичной команды не мешает этому: /proc/self/exe указывает на реально выполняемый бинарный файл. Если /proc/self/exe недоступен, используется резервное разрешение argv[0] через PATH и realpath(3); возврата к зашитому configure-time installation root нет.

Для бинарного файла:

<root>/bin/mcpu-cpp

runtime-корень определяется как:

executable        = <root>/bin/mcpu-cpp
executable dir    = <root>/bin
MCPU runtime root = <root>

Из него автоматически выводятся:

<root>/etc/mcpu-cpp.conf
<root>/include

Следовательно всё дерево можно физически перенести, например из:

/usr/lib64/mcpu/

в:

/opt/mcpu-test/

или:

$HOME/devel/mcpu-next/

и <new-root>/bin/mcpu-cpp без переконфигурирования начнёт использовать <new-root>/etc/mcpu-cpp.conf и <new-root>/include. Старый абсолютный путь не сохраняется ни в runtime default, ни в штатном mcpu-cpp.conf.

Это не частная особенность препроцессора, а общий принцип экосистемы MCPU. Будущие mcpu-as, mcpu-ld, mcpu-run, библиотеки, CRT и другие компоненты должны разделять один перемещаемый корень:

<root>/bin
<root>/etc
<root>/include
<root>/lib

Их собственные версии могут отличаться, но согласованность конкретной среды MCPU определяется тем, что все компоненты находятся в одном runtime tree, а не совпадением version suffix в именах каталогов.

4.2. Runtime defaults, уровни конфигурации и системный include root

До чтения любого конфигурационного файла MCPU-CPP создаёт runtime-derived значение:

MCPU_CPP_SYSTEM_INCLUDE_PATH = <runtime-root>/include

После этого конфигурационные слои применяются в порядке возрастающего приоритета:

runtime-derived defaults
        ↓
<runtime-root>/etc/mcpu-cpp.conf
        ↓
/etc/mcpu/mcpu-cpp.conf
        ↓
$HOME/.mcpu/mcpu-cpp.conf

<runtime-root>/etc/mcpu-cpp.conf устанавливается вместе с MCPU-CPP, но сам файл намеренно не содержит абсолютного штатного MCPU_CPP_SYSTEM_INCLUDE_PATH: иначе перенос всего дерева восстановил бы старый путь. /etc/mcpu/mcpu-cpp.conf является необязательным machine-wide override: make install каталог /etc/mcpu не создаёт. Домашний $HOME/.mcpu/mcpu-cpp.conf также необязателен, не версионируется и имеет максимальный config-приоритет.

Если одна переменная определена несколько раз, побеждает последнее определение, включая пустое. Поэтому MCPU_CPP_SYSTEM_INCLUDE_PATH остаётся полностью заменяемым system root. Например:

MCPU_CPP_SYSTEM_INCLUDE_PATH = $HOME/mcpu-next/include;

полностью заменяет runtime-derived <runtime-root>/include. Для активного #lang "as" тогда проверяются:

$HOME/mcpu-next/include/as
$HOME/mcpu-next/include

Штатные language-подкаталоги всегда выводятся самим препроцессором из одного root; переменных вида MCPU_CPP_SYSTEM_<LANG>_INCLUDE_PATH нет.

Пустое effective значение:

MCPU_CPP_SYSTEM_INCLUDE_PATH = ;

удаляет configured system stage полностью. Более приоритетный config может после этого снова включить его непустым значением.

--config-file FILE применяет явно выбранный файл поверх runtime-derived default. --no-config отключает только чтение файлов конфигурации: --sys-root PATH переводит <runtime-root> на PATH, где PATH может быть ка абсолютным, так и относительным путем: <runtime-root>/etc/mcpu-cpp.conf, /etc/mcpu/mcpu-cpp.conf и $HOME/.mcpu/mcpu-cpp.conf не читаются, но <runtime-root>/include остаётся штатным system root. Только -nostdinc удаляет effective standard-system tree из include search для конкретного запуска; явно переданный -isystem при этом остаётся command-line каталогом.

4.3. Нормативный порядок поиска include-файлов

Порядок поиска является частью контракта MCPU-CPP. Явно заданные параметры командной строки имеют приоритет над persistent configuration. После необязательного каталога текущего физического файла эффективная цепочка имеет строго следующий вид:

explicit -I
        ↓
explicit -isystem
        ↓
MCPU_CPP_<LANG>_INCLUDE_PATH
        ↓
MCPU_CPP_INCLUDE_PATH
        ↓
MCPU_CPP_SYSTEM_INCLUDE_PATH/<lang>
        ↓
MCPU_CPP_SYSTEM_INCLUDE_PATH
        ↓
explicit -idirafter
        ↓
MCPU_CPP_AFTER_INCLUDE_PATH

Элементы, которых нет или которые не содержат требуемого файла, пропускаются.

MCPU_CPP_<LANG>_INCLUDE_PATH – свободно настраиваемые пользователем language-specific path-list'ы:

MCPU_CPP_DIFF_INCLUDE_PATH
MCPU_CPP_DIFT_INCLUDE_PATH
MCPU_CPP_ALG_INCLUDE_PATH
MCPU_CPP_AS_INCLUDE_PATH
MCPU_CPP_AVM_INCLUDE_PATH
MCPU_CPP_ACS_INCLUDE_PATH

Пользователь полностью распоряжается именами и расположением этих каталогов. MCPU_CPP_INCLUDE_PATH – общий пользовательский path-list, видимый во всех языковых состояниях.

-idirafter и MCPU_CPP_AFTER_INCLUDE_PATH являются общим fallback-карманом. MCPU-CPP не строит для них автоматических <lang> -подкаталогов. Пользователь сам организует их внутреннюю структуру и при необходимости пишет, например:

#include <vendor/device.h>

Именно semantic class, а не порядок появления разных классов в argv/config, определяет приоритет. Внутри одного класса сохраняется порядок добавления.

4.4. #include_next и wrapper headers

#include_next предназначен прежде всего для заголовков-обёрток (wrapper headers). Он позволяет поставить локальный header раньше системного, изменить локальную политику и затем продолжить поиск одноимённого header по нормативной цепочке без копирования системного файла и без абсолютного имени.

Например:

mcpu-cpp -isystem $HOME/mcpu-wrapper ...

и $HOME/mcpu-wrapper/math.h:

#ifndef SOME_SYSTEM_MACRO
#define SOME_SYSTEM_MACRO temporary_value
#define REMOVE_SOME_SYSTEM_MACRO 1
#endif

#include_next <math.h>

#ifdef REMOVE_SOME_SYSTEM_MACRO
#undef SOME_SYSTEM_MACRO
#undef REMOVE_SOME_SYSTEM_MACRO
#endif

Если домашний config одновременно задаёт:

MCPU_CPP_SYSTEM_INCLUDE_PATH = $HOME/mcpu-next/include;

wrapper найденный через -isystem продолжит #include_next уже через configured user paths, затем через $HOME/mcpu-next/include/<lang> и $HOME/mcpu-next/include; старое system tree исходного места установки при этом не участвует. Именно такой сценарий позволяет системному разработчику или тестеру жить в собственной sandbox.

MCPU-CPP хранит конкретный физический элемент effective search chain, из которого найден текущий header. #include_next начинает со следующего элемента. Формы "file" и <file> для #include_next эквивалентны; каталог текущего файла повторно не проверяется. Если текущий файл найден обычным quoted-поиском относительно содержащего файла и не имеет search-chain provenance, #include_next начинает с первого элемента configured chain.

Операнд может быть получен macro expansion. Логическое имя после #line не влияет на физический provenance. Если после текущего entry подходящего файла нет, preprocessing завершается ошибкой.

4.5. #pragma once

Активная директива

#pragma once

помечает физический файл как уже обработанный в текущем запуске MCPU-CPP. При последующей попытке включить тот же физический файл его содержимое повторно не обрабатывается. Сама директива потребляется препроцессором и в выходной поток не копируется, в том числе при -dD.

Идентичность определяется по паре st_dev/st_ino, полученной файловой системой, а не по строковому имени пути. Поэтому один и тот же файл не может обойти #pragma once, если он достигнут как ./file.h, через символическую ссылку или через другое жёсткое имя (hard link). Логическое имя после #line также не влияет на эту физическую идентичность.

Пометка действует сразу в момент обработки активной директивы. Поэтому заголовок может после #pragma once включить самого себя: повторное включение будет пропущено и рекурсия не возникнет. Директива внутри неактивной ветви условной компиляции никакого действия не имеет.

MCPU-CPP распознаёт только точную форму #pragma once с необязательными пробелами. Остальные #pragma не интерпретируются препроцессором и сохраняются для последующих стадий компиляции; например, #pragma pack(...) продолжает передаваться в выходной поток.

#pragma once дополняет, но не изменяет нормативную search-chain #include/#include_next: сначала обычный механизм поиска находит физический файл, затем registry once решает, надо ли обрабатывать его содержимое.

4.6. Принудительные файлы: -imacros FILE и -include FILE

Опции командной строки

-imacros FILE
-include FILE

обрабатывают файл до главного input. Они используют обычный preprocessing engine, а не отдельный облегчённый parser.

Нормативный порядок начала translation unit:

predefined macros
    -> -D/-U в порядке командной строки
    -> все -imacros в порядке командной строки
    -> все -include в порядке командной строки
    -> главный input

Таким образом, взаимное расположение -imacros и -include в argv не перемешивает эти две группы: все -imacros всегда выполняются раньше всех -include.

-imacros FILE полностью обрабатывает файл: его #define/ #undef, условные директивы, #lang/ #endlang, #include, #include_next, #pragma once и диагностика имеют обычную семантику. Однако весь normal preprocessing output этого forced-файла, включая line markers и текст вложенных headers, отбрасывается. Полученное состояние macro table и других preprocessing-механизмов сохраняется для последующих forced-файлов и главного input.

-include FILE использует тот же механизм, но normal output сохраняется, как если бы найденный header был включён непосредственно перед главным source. Forced include является настоящей include-границей: внутри него __INCLUDE_LEVEL__ == 1, внутри включённого им header уровень равен 2, а главный input остаётся на уровне 0. __BASE_FILE__ внутри forced-файлов остаётся именем главного input.

Абсолютный operand forced-файла используется непосредственно. Относительный operand ищется сначала в current working directory, затем по обычной include-chain:

explicit -I
explicit -isystem
MCPU_CPP_<LANG>_INCLUDE_PATH
MCPU_CPP_INCLUDE_PATH
MCPU_CPP_SYSTEM_INCLUDE_PATH/<lang>
MCPU_CPP_SYSTEM_INCLUDE_PATH
explicit -idirafter
MCPU_CPP_AFTER_INCLUDE_PATH

Каталог главного input не получает специального приоритета при поиске operand -imacros /-include. После нахождения forced-файла обычный quoted #include "file" внутри него снова разрешается относительно физического каталога этого файла. Если forced-файл найден через элемент include-chain, его provenance сохраняется и #include_next продолжает поиск со следующего элемента цепочки.

Forced-файлы и реально достигнутые из них headers входят в обычный physical dependency registry. Их user/system classification определяется тем же search provenance, поэтому -MM/ -MMD фильтруют system forced headers так же, как обычные system headers. Отсутствующий forced-файл является ошибкой.

4.7. Генерация зависимостей: -M, -MM, -MG, -MD, -MMD, -MF, -MT, -MQ

Опции -M и -MM используют тот же самый проход include pipeline, что и обычная preprocessing. Отдельного повторного поиска заголовков не выполняется. Поэтому dependency graph автоматически наследует нормативный порядок путей, #include_next, macro-expanded include operands, conditional compilation и #pragma once.

-M подавляет обычный preprocessing output и выводит одно правило Make:

file.o: file.c header1.h header2.h

В список входят главный source-файл и все реально достигнутые физические headers, включая system headers. Один физический файл записывается один раз; идентичность определяется как st_dev + st_ino, поэтому другое относительное имя, symbolic link или hard link не создают дополнительную dependency. Имя, назначенное директивой #line, является только logical source name и в dependency list не попадает.

-MM строит тот же граф, но исключает system dependencies. System-контекстом считаются headers, найденные через explicit -isystem, configured system tree MCPU_CPP_SYSTEM_INCLUDE_PATH/<lang> / MCPU_CPP_SYSTEM_INCLUDE_PATH, explicit -idirafter и MCPU_CPP_AFTER_INCLUDE_PATH, а также вся ветвь headers, включённая непосредственно или косвенно из такого system header. Форма #include "file" или #include <file> сама по себе не определяет system-ness. Если один и тот же физический файл был достигнут из system-ветви, но затем также включён непосредственно из user-контекста, он остаётся пользовательской dependency и присутствует в -MM.

Default target образуется из basename главного source-файла: его suffix заменяется object suffix ( .o по умолчанию). Пути и target экранируются для Make. Для stdin используется GNU-подобная форма -: -.

-MD и -MMD используют тот же dependency graph, но, в отличие от -M и -MM, не подавляют обычный preprocessing output. -MD включает system headers, как -M; -MMD применяет user-only фильтр -MM. Это позволяет одним проходом получить и препроцессированный текст, и side-effect dependency file.

Если -MF не задан, side-effect режим выбирает имя .d автоматически:

  • без -o из basename входного файла удаляется suffix и добавляется .d; каталоги входного pathname в имя dependency-файла не переносятся;
  • при обычном -o FILE suffix output-файла заменяется на .d;
  • для stdin используется имя -.d.

-MF FILE переопределяет автоматическое имя dependency-файла. Значение -MF - означает stdout. -MF работает также с dependency-only -M/-MM; в этом случае оно имеет приоритет над обычным destination для make-rule. Само по себе -MF без одного из -M , -MM, -MD, -MMD является ошибкой.

Семантика намеренно следует GNU CPP: -MD/-MMD не принимают собственный аргумент, а -MF является отдельной опцией назначения dependency output.

-MT TARGET заменяет автоматический target правила строкой TARGET точно как она передана. Make quoting при этом не выполняется. Поэтому один argument -MT может сам содержать несколько targets, разделённых пробелами:

-MT 'obj/a.o obj/a.pic.o'

и повторные -MT также добавляют targets одного и того же правила:

-MT obj/a.o -MT obj/a.pic.o

-MQ TARGET имеет ту же семантику выбора target, но экранирует специальные для Make символы. Например,

-MQ '$(OBJDIR)/foo.o'

даёт левую часть правила:

$$(OBJDIR)/foo.o:

Поддерживаются как отдельные arguments (-MT TARGET, -MQ TARGET), так и attached forms (-MTTARGET, -MQTARGET).

Если задан хотя бы один -MT или -MQ, автоматический default target не выводится. В частности, --object-suffix влияет только на автоматический target и не переписывает явно заданные targets. Если явных targets нет, default target экранируется для Make так же, как при -MQ.

Разрешены повторные и смешанные -MT/-MQ. В соответствии с GNU CPP сначала выводятся все -MT targets в их command-line order, затем все -MQ targets в их command-line order. Все они образуют левую часть одного dependency rule.

-MT и -MQ имеют смысл только вместе с одним из dependency-generation режимов -M, -MM, -MD или -MMD. Без такого режима это ошибка командной строки.

-MG изменяет только обработку отсутствующих include-файлов при dependency-only режимах -M и -MM. Без -MG неразрешённый #include остаётся ошибкой. С -M -MG или -MM -MG отсутствующий header считается будущим generated file: preprocessing не завершается ошибкой, а operand директивы добавляется в dependency rule ровно в том виде, который получен после macro expansion, без приписывания предполагаемого include-directory. Например:

#include "generated.h"

при -M -MG добавляет dependency generated.h, даже если такого файла ещё нет. Macro-expanded include ведёт себя аналогично: dependency получает уже развёрнутое имя. -MG разрешён только вместе с -M или -MM; комбинации с -MD/-MMD и использование без dependency-only режима являются ошибкой командной строки.

Unresolved dependencies интегрированы в тот же упорядоченный dependency registry, что и физически найденные файлы, но образуют отдельный identity domain. Для найденного файла registry по-прежнему использует st_dev/st_ino и physical provenance. Для отсутствующего файла этих данных нет, поэтому -MG entry не выполняет stat() и дедуплицируется по точному тексту include operand. Это принципиально: наличие одноимённого файла в CWD не должно превращать неразрешённый <name> в ложное physical совпадение, если angle-search этот файл не находил. Разные unresolved spellings (generated.h и ./generated.h) считаются разными dependencies.

Для -MM unresolved dependency получает user/system class из контекста поиска: отсутствующий <file> является system-class, отсутствующий "file" – user-class, если сама включающая единица не является system header; любой missing include, достигнутый из system header, остаётся system-class. При повторении одного и того же unresolved operand сохраняется классификация его первого появления, что соответствует GNU CPP. Физически найденные зависимости сохраняют прежнее правило: если один и тот же inode позднее достигается из user-контекста, он перестаёт быть system-only.

-MG распространяется также на отсутствующие command-line forced files -include FILE и -imacros FILE: их operand заносится в unresolved registry как user dependency без синтетического search prefix. При наличии реального файла -include/-imacros продолжают использовать обычный physical dependency registry и search provenance.

Практический Makefile с dependency-файлами

Для собственного Makefile особенно удобен режим -MMD: один запуск mcpu-cpp одновременно создаёт препроцессированный файл и side-effect .d с реально использованными пользовательскими заголовками. Явный -MF задаёт имя dependency-файла, а -MT связывает правило с тем target, который действительно строит Make.

MCPU_CPP := mcpu-cpp
CPPFLAGS := -Iinclude

SOURCES  := $(wildcard src/*.s)
PPFILES  := $(patsubst src/%.s,build/%.S,$(SOURCES))
DEPS     := $(PPFILES:.S=.d)

build/%.S: src/%.s
	@mkdir -p $(@D)
	$(MCPU_CPP) $(CPPFLAGS) -MMD -MF $(@:.S=.d) -MT $@ -o $@ $<

-include $(DEPS)

После первого запуска, например, build/model.d может содержать обычное правило Make. Изменение любого header, который действительно прошёл через include pipeline, автоматически приводит к перестроению соответствующего preprocessing target. Header из неактивной ветви #if в dependency rule не появляется.

build/model.S: src/model.s include/model.h include/constants.h

5. Переключение языков

Препроцессор запускается в состоянии 0. Это безымянный основной C-подобный язык и он не является допустимым аргументом #lang.

Допустимые языки:

Имя Назначение
diff дифференциальные уравнения
dift разностные уравнения
alg алгебраические уравнения
as MCPU assembler (mcpu-as)
avm схемы аналоговых вычислительных машин
ACS структурные схемы систем автоматического управления

После #lang обязательна строковая константа с одним непустым словом:

#lang "diff"

Имя проверяется только по внутреннему списку языков выше и сравнивается без учёта ASCII-регистра. Поэтому "diff", "Diff", "DIFF" и "dIfF" эквивалентны при выборе языка. Исходное написание внутри кавычек при этом сохраняется в выходном потоке.

Пробелы внутри строковой константы запрещены: " diff", "diff " и "di ff" являются ошибками. Escape-последовательности внутри неё не разбираются. Закрывающая кавычка обязана находиться на той же физической строке исходного файла. После неё до конца строки допустимы только пробельные символы.

Внешние пробелы директивы нормализуются. Например:

               #   lang   "DiFf"          

превращается в:

#lang "DiFf"

#lang помещает новый язык в стек, #endlang восстанавливает предыдущий. Стек не сбрасывается при #include, поэтому начало и конец языкового блока могут находиться в разных файлах. Директивы #lang и #endlang сохраняются в выходном потоке для последующего frontend dispatcher; #lang сохраняется в нормализованной форме.

6. Простые макроопределения

Начиная с 0.0.4 поддерживаются object-like macros:

#define BUFFER_SIZE 1024
#define NAME value
#define EMPTY

Директива #define сама в выходной поток не попадает. В обычном тексте идентификатор-макро заменяется его replacement list. Replacement затем снова просматривается на макроимена, поэтому допускается каскадное раскрытие:

#define A B
#define B 10
A

даёт 10.

Во время раскрытия конкретное макро временно блокируется. Поэтому самоссылочные и взаимно-рекурсивные определения не вызывают бесконечной рекурсии.

Макроимена не раскрываются внутри строковых и символьных констант. Для diff апостроф сохраняет специальную языковую семантику и не защищает последующий текст как C character constant.

Многострочное определение через backslash-newline поддерживается, поскольку splice выполняется раньше #define.

6.1. #undef

#undef NAME

удаляет object-like macro. Отмена несуществующего определения не является ошибкой.

6.2. Вычисляемый #include

Аргумент #include, который не начинается непосредственно с " или <, сначала проходит macro expansion. Поэтому допустимо:

#define HEADER <diff/model.h>
#include HEADER

или

#define HEADER "local.h"
#include HEADER

Результат раскрытия обязан иметь форму "file" или <file>.

7. Макро с аргументами

Macro engine поддерживает function-like macros:

#define идентификатор( список аргументов ) текст

Открывающая скобка в определении должна идти непосредственно после имени макро. Поэтому

#define F(X) X

задаёт макро с аргументом, а

#define F (X)

задаёт простое object-like macro со строкой замены (X).

В месте использования между именем function-like macro и открывающей скобкой пробельные символы допустимы. Если ( не следует, идентификатор не считается вызовом данного макро и остаётся в выходном тексте.

Для обычного function-like macro число фактических аргументов должно совпадать с числом формальных. Для variadic macro должны присутствовать все фиксированные аргументы, а variadic tail может содержать произвольное число аргументов, включая пустой tail. При разборе списка фактических аргументов вложенные круглые скобки учитываются; запятая внутри них не разделяет аргументы. Квадратные скобки такого свойства не имеют – это является частью принятой семантики macro expansion.

Например,

#define min(X, Y) ((X) < (Y) ? (X) : (Y))
min(1, 2)

даёт

((1) < (2) ? (1) : (2))

Перед подстановкой обычный фактический аргумент сам проходит macro expansion. Поэтому каскадные и вложенные вызовы работают естественно:

#define A 7
#define min(X, Y) ((X) < (Y) ? (X) : (Y))
min(min(A, 3), 10)

Формальный параметр может встречаться в replacement list произвольное число раз. Это означает, что выражение с побочным эффектом в фактическом аргументе также может быть вычислено несколько раз уже последующим компилятором; препроцессор не пытается исправлять такую программу.

Поддерживаются макро без формальных параметров:

#define READY() 1

Они раскрываются только как вызов READY() (пробел между именем и ( при использовании допустим), но самостоятельный идентификатор READY не раскрывается.

Имена формальных параметров должны быть различны. Незавершённый список, неверная пунктуация, недостаточное или избыточное число фактических аргументов диагностируются как ошибки.

7.1. Stringification #

Поддерживается оператор stringification (#) для параметров function-like macro:

#define STR(X) #X
STR(alpha + beta)

даёт

"alpha + beta"

Stringification использует сырой фактический аргумент до macro expansion. Поэтому:

#define A 7
#define STR(X) #X
#define XSTR(X) STR(X)

STR(A)   -> "A"
XSTR(A)  -> "7"

Ведущие и завершающие пробелы аргумента удаляются. Последовательности пробельных символов внутри аргумента сворачиваются в один пробел, кроме пробелов внутри строковых/символьных токенов соответствующего активного языка. Двойные кавычки и обратные косые черты внутри quoted tokens экранируются так, чтобы результат оставался одной корректной строковой константой.

Оператор # в replacement list function-like macro обязан непосредственно или через пробельные символы ссылаться на имя формального параметра. Внутри quoted token символ # оператором не является. Пустой фактический аргумент допустим и stringify-ится как "".

7.2. Token concatenation ##

Начиная с 0.0.21 поддерживается оператор token concatenation (##) в модели, согласованной с GNU CPP и механизмом collect_expansion() / macroexpand() macro engine. Оператор объединяет два соседних preprocessing token в один token, после чего получившийся replacement list снова проходит macro expansion.

Например:

#define CAT(A, B) A ## B
CAT(foo, bar)

даёт foobar. Склеивание может образовывать identifier, preprocessing number или многосимвольный punctuator. Поэтому, например, допустимы:

CAT(1.5, e3) -> 1.5e3
CAT(+, =)    -> +=

Если формальный параметр непосредственно примыкает к ##, его фактический аргумент подставляется без предварительного macro expansion. Это тот же raw-argument принцип, который используется для stringification. Для получения сначала expansion, а затем concatenation применяется обычный двухуровневый приём GNU CPP:

#define AFTERX(X) X_ ## X
#define XAFTERX(X) AFTERX(X)
#define TABLESIZE 1024
#define BUFSIZE TABLESIZE

AFTERX(BUFSIZE)  -> X_BUFSIZE
XAFTERX(BUFSIZE) -> X_1024

Пустой фактический аргумент рядом с ## ведёт себя как placemarker: сам по себе он не добавляет token, а ## с такой стороны не изменяет оставшийся операнд. Если фактический аргумент содержит несколько preprocessing tokens, склеивается только крайний token, непосредственно соседний с ##; остальные tokens сохраняются и затем участвуют в общем rescan.

# и ## могут использоваться в одном function-like macro, например:

#define COMMAND(NAME) #NAME | NAME ## _command

При этом #NAME использует raw spelling аргумента для stringification, а NAME ## _command – тот же raw argument для concatenation.

## внутри quoted token оператором не является. Комментарии к моменту macro expansion уже заменены whitespace, поэтому они не могут быть созданы склеиванием / и *. Между ## и его операндами исходно может находиться whitespace; при склеивании он не участвует.

Если два операнда не образуют один допустимый preprocessing token, выдаётся диагностика, а сами исходные tokens сохраняются; наличие whitespace между ними после такой диагностики не является частью контракта. ## в начале или в конце replacement list является ошибкой определения macro.

7.3. Variadic macros: ... и __VA_ARGS__

Начиная с 0.0.46 поддерживаются variadic function-like macros в современной C99-совместимой форме:

#define LOG(...) output(__VA_ARGS__)
#define LOGF(format, ...) output(format, __VA_ARGS__)

Маркер ... может быть единственным параметром либо последним элементом после одного или нескольких фиксированных параметров. Старое GNU-расширение с именованным variadic parameter

#define LOG(args...) ...

в 0.0.46 намеренно не поддерживается. __VA_OPT__ также не является частью этого релиза.

При вызове все tokens после последнего фиксированного параметра, включая разделяющие их запятые, образуют один logical variable argument и подставляются вместо __VA_ARGS__. В обычной позиции этот variable argument предварительно проходит macro expansion так же, как обычный фактический аргумент:

#define A 7
#define V(...) <__VA_ARGS__>
#define F(first, ...) first | __VA_ARGS__

V(A, 2, 3)  -> <7, 2, 3>
F(1, A, 3)  -> 1 | 7, 3

Variadic tail может быть пустым. Поэтому оба вызова

F(1)
F(1,)

допустимы и подставляют пустой __VA_ARGS__. Это не означает автоматическое удаление запятой, явно записанной в replacement list. Например для

#define E(format, ...) output(format, __VA_ARGS__)

вызов E("ok") оставляет запятую перед пустым tail. Специальная историческая GNU-семантика , ## __VA_ARGS__, удаляющая такую запятую, в контракт 0.0.46 не входит; если она понадобится, её следует вводить отдельным явно документированным расширением.

__VA_ARGS__ участвует в уже существующей семантике # и ## как настоящий macro parameter. Stringification использует raw spelling всего variadic tail:

#define STRV(...) #__VA_ARGS__
STRV(A, b + c) -> "A, b + c"

При соседстве с ## variadic argument также подставляется без prescan; затем работают обычные правила placemarker, token concatenation и общего rescan. Например:

#define L(...) pre ## __VA_ARGS__
#define R(...) __VA_ARGS__ ## post

L(fix) -> prefix
R(fix) -> fixpost

Если variadic argument содержит несколько preprocessing tokens, склеивается только крайний token, непосредственно соседний с ##, а остальные tokens сохраняются, как и для обычного параметра. Пустой tail рядом с ## ведёт себя как placemarker.

Имя __VA_ARGS__ зарезервировано для variable argument и не принимается как обычное имя формального параметра. Оператор #__VA_ARGS__ допустим только в variadic macro. Dump-режимы сохраняют variadic форму определения, например:

#define F(first,...) first | __VA_ARGS__

7.4. __VA_OPT__

Начиная с 0.0.47 variadic macros поддерживают стандартный условный fragment __VA_OPT__(pp-tokens). Если variable argument после обычной macro substitution не содержит preprocessing tokens, весь __VA_OPT__(...) раскрывается в пустую последовательность. Если variable argument непуст, содержимое круглых скобок участвует в replacement list:

#define DEBUG(format, ...) \
        fprintf(stderr, format __VA_OPT__(,) __VA_ARGS__)

DEBUG("ready")       -> fprintf(stderr, "ready")
DEBUG("x=%d", x)     -> fprintf(stderr, "x=%d", x)

Решение о непустоте принимается после expansion variable argument, а не по его исходному spelling. Поэтому macro, который сам раскрывается в пустую последовательность, не активирует __VA_OPT__:

#define EMPTY
#define HAS(...) [__VA_OPT__(yes)]

HAS()       -> []
HAS(EMPTY)  -> []
HAS(token)  -> [yes]

Содержимое __VA_OPT__ может включать сбалансированные вложенные круглые скобки. Закрывающая ) самого __VA_OPT__ определяется с учётом их вложенности. Вложенный __VA_OPT__ внутри другого __VA_OPT__ намеренно запрещён.

__VA_OPT__ интегрирован с существующими правилами parameter substitution, stringification, token concatenation, placemarker и rescan. Например:

#define X 123
#define S(...) #__VA_OPT__(__VA_ARGS__)
#define L(...) pre ## __VA_OPT__(__VA_ARGS__)

S()     -> ""
S(X)    -> "123"
L()     -> pre
L(X)    -> pre123

При #__VA_OPT__(...) сначала выполняется parameter substitution внутри fragment, включая prescan обычных параметров, но произвольные macro names самого fragment до stringification дополнительно не rescanning-ятся. Поэтому:

#define X 123
#define S(a, ...) #__VA_OPT__(a X)

S(X, y) -> "123 X"

Если parameter внутри __VA_OPT__ непосредственно участвует во внутреннем ##, для него, как обычно, prescan подавляется; paste выполняется до дальнейшего rescan. Внешний ##, соседний с __VA_OPT__, получает крайний token уже подготовленного fragment. Пустой результат __VA_OPT__ рядом с ## ведёт себя как placemarker.

__VA_OPT__ допустим только в replacement list variadic function-like macro и должен непосредственно задавать parenthesized fragment. ## не может быть первым или последним preprocessing token внутри самого __VA_OPT__.

Историческое GNU-расширение

, ## __VA_ARGS__

в mcpu-cpp намеренно не реализуется. Для условной запятой следует использовать современную форму __VA_OPT__(,). Старое GNU-расширение с именованным variadic parameter args... также остаётся неподдерживаемым.

7.5. Нормализация пробелов в replacement list

Начиная с 0.0.48 mcpu-cpp не переносит в результат разворачивания служебное выравнивание многострочного macro. После удаления \ + newline последовательность пробельных символов, принадлежащая самому replacement list, канонизируется в один ASCII-пробел. Это особенно важно для определений, где обратные косые черты визуально выровнены в одну колонку:

#define TRACE(x)                                                \
        do                                                      \
        {                                                       \
                output(x);                                      \
                done();                                         \
        }                                                       \
        while( 0 )

При разворачивании такое определение выдаёт компактный replacement:

do { output(x); done(); } while( 0 )

а не сохраняет десятки пробелов перед каждой бывшей границей физической строки.

Нормализация относится только к whitespace самого replacement list. mcpu-cpp не является formatter-ом исходной программы: пробелы в обычном тексте input сохраняются. Пробелы внутри фактического macro argument также не переформатируются только потому, что argument был подставлен в macro:

#define ID(x) x

ID(a        +        b) -> a        +        b

Содержимое string/character literals сохраняется буквально, поэтому:

#define S "left     right"

по-прежнему содержит пять пробелов внутри строки.

Наличие whitespace между preprocessing tokens сохраняется как один пробел. Это не позволяет случайно изменить tokenization, например превратить + + в ++, - > в -> или < < в <<. Операторы # и ##, placemarkers, __VA_ARGS__, __VA_OPT__ и последующий rescan продолжают использовать свои существующие правила; новая политика меняет только количество обычного replacement-list whitespace.

Dump-режимы (-dM, -dD) показывают ту же каноническую форму replacement list, которая хранится во внутренней таблице macro.

7.6. Компактификация невидимых строк и linemarkers

Начиная с 0.0.49 mcpu-cpp использует для вертикального whitespace ту же модель, что GNU CPP: удаляем, но не забываем. Строки, которые после preprocessing не породили ни одного выводимого preprocessing token, не обязаны оставаться физическими пустыми строками в .E, однако их исходная позиция продолжает учитываться при построении linemarkers и значении __LINE__.

Причина невидимости не имеет значения. Это могут быть удалённые directives, неактивные ветви #if, однострочные и многострочные comments, обычные пустые строки или их смесь. Emitter сравнивает текущую output source position с позицией следующей реально выдаваемой строки.

Если следующая позиция находится менее чем через восемь строк, разрыв представляется обычными newline. Если расстояние равно восьми строкам или больше, вместо длинной последовательности пустых строк выдаётся корректирующий linemarker:

# N "file"

и следующая содержательная строка сразу относится к source line N. Таким образом граница поведения совместима с GNU CPP: gaps 0..7 сохраняются через newline, gap 8 и больше заменяется linemarker.

Structural markers входа и возврата из include-файла сохраняют обычный смысл:

# 1 "header.h" 1
# 4 "source.c" 2

Если included file не породил никакого output, mcpu-cpp не создаёт искусственный marker, сообщающий, до какой внутренней строки header дошёл препроцессор. После enter-marker сразу может следовать return-marker. Реальная позиция снова уточняется только тогда, когда требуется выдать следующий содержательный текст.

Эта оптимизация меняет только представление output stream. Source coordinates, __LINE__, diagnostics, #line, include enter/return semantics и обработка macro остаются привязаны к исходному логическому потоку, а не к количеству физических строк в сжатом .E.

8. Предопределённые макро

Начиная с 0.0.6 был перенесён исторический механизм predefined macros из препроцессора. Этот механизм оформлен как отдельный ABI/environment layer будущего безымянного C-подобного языка. Эти определения не являются декоративными: их имена и значения должны соответствовать либо семантике GNU CPP, либо явно документированному MCPU/LibMPU contract.

8.1. Динамические source macros

Следующие predefined macros вычисляются в точке использования:

Макро Раскрытие
__FILE__ строковая константа с именем текущего входного файла
__LINE__ десятичный номер текущей строки
__BASE_FILE__ строковая константа с именем главного входного файла translation unit
__INCLUDE_LEVEL__ уровень вложенности #include; для главного файла равен 0
__DATE__ дата запуска препроцессора в форме "Mmm dd yyyy"
__TIME__ время запуска препроцессора в форме "hh:mm:ss"

__DATE__ и __TIME__ получают один timestamp на весь translation unit. Специальное раскрытие помещается в output без повторного macro rescan.

Эти имена находятся в общей macro table, поэтому #undef и последующий #define могут осознанно заменить builtin.

8.2. Версия препроцессора

Начиная с 0.0.8 standalone preprocessor не определяет GCC-имя __VERSION__. Оно относится к compiler environment, которого для будущего high-level языка пока нет. Собственная версия mcpu-cpp имеет отдельное однозначное имя:

#define __MCPU_CPP_VERSION__ "1.0.2"

Значение автоматически берётся из PACKAGE_VERSION. Когда появится compiler frontend/driver, его version contract будет определён отдельно и не будет смешиваться с версией standalone preprocessor.

8.3. Источники истины ABI

mcpu-cpp собирается только GNU GCC. Во время configure проект использует проверенные приёмы из LibMPU/LibMPUIO acsite.m4: GCC predefined macros определяют native type sizes, byte/word order и machine-register width, а установленный <libmpu.h> является окончательным источником настроек LibMPU.

В частности, фиксируются и проверяются:

MPU_REAL_IO_LIMIT
MPU_MATH_FN_LIMIT
MPU_BYTE_ORDER
MPU_WORD_ORDER
BITS_PER_MACHINE_REGISTER
BITS_PER_UNIT_T
sizeof(__mpu_size_t)
sizeof(__mpu_ptrdiff_t)

configure дополнительно проверяет, что byte order и BITS_PER_MACHINE_REGISTER, записанные в LibMPU, согласованы с GCC target, которым собирается mcpu-cpp. MPU_WORD_ORDER берётся непосредственно из configured LibMPU profile и описывает порядок слов MCPU data environment.

Пределы MPU_REAL_IO_LIMIT и MPU_MATH_FN_LIMIT имеют разные назначения. Например, библиотека может иметь Real I/O до 65536 бит и математические функции только до 16384 бит. Поэтому MPU_MATH_FN_LIMIT не используется как предел существования типов Real.

8.4. MCPU architecture и assembler prefixes

Целевая архитектура определяется макро:

#define _ARCH_MCPU 1

MCPU PTR64 имеет ширину 64 бита, поэтому определены __SIZEOF_POINTER__, __MCPU_POINTER_WIDTH__, __INTPTR_TYPE__, __UINTPTR_TYPE__, соответствующие width/max macros.

Смысл assembler-prefix macros согласован с GNU CPP, а не с первой буквой имени register view. В синтаксисе mcpu-as дополнительного sigil перед register, label или immediate нет. r и c являются частью MCPU register syntax, а не REGISTER_PREFIX. Поэтому:

#define __REGISTER_PREFIX__
#define __LOCAL_LABEL_PREFIX__
#define __USER_LABEL_PREFIX__
#define __IMMEDIATE_PREFIX__

все четыре раскрываются в пустую последовательность. .L... остаётся compiler naming convention и не является assembler ABI local-label prefix: LOCAL/GLOBAL binding определяется symbol directives.

8.5. Byte order и word order

Базовые числовые значения порядка байт совместимы с GNU CPP:

__ORDER_LITTLE_ENDIAN__
__ORDER_BIG_ENDIAN__
__ORDER_PDP_ENDIAN__

Но целевая среда публикует собственные MCPU names:

#define __MCPU_BYTE_ORDER__ __ORDER_LITTLE_ENDIAN__
#define __MCPU_WORD_ORDER__ __ORDER_LITTLE_ENDIAN__
#define __BYTE_ORDER__ __MCPU_BYTE_ORDER__

Фактические значения __MCPU_BYTE_ORDER__ и __MCPU_WORD_ORDER__ получают из configured LibMPU profile ( MPU_BYTE_ORDER и MPU_WORD_ORDER). Поэтому они следуют host data representation, с которой собрана LibMPU. Это не меняет отдельный архитектурный контракт кодировки MCPU instruction bytecode.

GNU/C-specific имя __FLOAT_WORD_ORDER__ не определяется: типа float в будущем языке MCPU нет.

Параметры LibMPU/MCPU environment публикуются в MCPU namespace:

__MCPU_MACHINE_REGISTER_WIDTH__
__MCPU_REAL_IO_LIMIT__
__MCPU_MATH_FN_LIMIT__
__MCPU_INT_MAX_WIDTH__
__MCPU_REAL_MAX_WIDTH__
__MCPU_COMPLEX_MAX_WIDTH__

__MCPU_INT_MAX_WIDTH__ равен NB_I_MAX * 8, а Real/Complex maximum width равен configured MPU_REAL_IO_LIMIT. __MCPU_MACHINE_REGISTER_WIDTH__ является значением BITS_PER_MACHINE_REGISTER установленной LibMPU. Пределы Real I/O и math functions не смешиваются: MPU_REAL_IO_LIMIT определяет существование Real/Complex type family и text conversion, а MPU_MATH_FN_LIMIT – наличие математических функций соответствующей ширины.

8.6. MCPU size/ssize, ptrdiff и pointers

Будущий язык не наследует variable-width C names short, int, long и не использует C-style имя size_t как часть собственного ABI. Беззнаковый LibMPU size type и знаковый byte-count/error type публикуются симметрично в MCPU namespace. Например для 64-bit configured profile:

#define __MCPU_SIZE_TYPE__ uint64
#define __MCPU_SIZE_WIDTH__ 64
#define __MCPU_SIZEOF_SIZE__ 8
#define __MCPU_SIZE_MAX__ 0xffffffffffffffff

#define __MCPU_SSIZE_TYPE__ int64
#define __MCPU_SSIZE_WIDTH__ 64
#define __MCPU_SIZEOF_SSIZE__ 8
#define __MCPU_SSIZE_MAX__ 0x7fffffffffffffff

Это MCPU-specific family, а не попытка приписать GNU CPP несуществующий стандартный __SSIZE_* contract.

MCPU pointer ABI от host не зависит: PTR64 всегда имеет ширину 64 бита:

#define __INTPTR_TYPE__ int64
#define __UINTPTR_TYPE__ uint64
#define __INTPTR_WIDTH__ 64
#define __UINTPTR_WIDTH__ 64
#define __INTPTR_MAX__ 0x7fffffffffffffff
#define __UINTPTR_MAX__ 0xffffffffffffffff
#define __SIZEOF_POINTER__ 8
#define __MCPU_POINTER_WIDTH__ 64

Разность MCPU pointers является знаковой и также фиксирована независимо от host:

#define __PTRDIFF_TYPE__ int64
#define __PTRDIFF_WIDTH__ 64
#define __SIZEOF_PTRDIFF__ 8
#define __PTRDIFF_MAX__ 0x7fffffffffffffff

Computed MIN expressions вроде (-__PTRDIFF_MAX__ - 1) в predefined table не создаются.

8.7. Character types

Обычного C char в будущем языке нет. Поэтому __CHAR_TYPE__ и __WCHAR_TYPE__ не определяются. Типы языка называются без C/C++ suffix _t:

#define __CHAR8_TYPE__  char8
#define __CHAR16_TYPE__ char16
#define __CHAR8_WIDTH__ 8
#define __CHAR16_WIDTH__ 16
#define __SIZEOF_CHAR8__ 1
#define __SIZEOF_CHAR16__ 2

Это типы будущего языка. Внутренняя реализация самого mcpu-cpp по-прежнему использует LibMPUIO __mpu_char16_t и strict UCS-2 text model.

8.8. Integer families LibMPU

Полная structural metadata integer families строится не по жёстко записанному последнему типу, а до NB_I_MAX * 8 фактически установленной LibMPU. Для каждой power-of-two ширины от 8 бит определяются TYPE, WIDTH и SIZEOF:

#define __INT1024_TYPE__ int1024
#define __UINT1024_TYPE__ uint1024
#define __INT1024_WIDTH__ 1024
#define __UINT1024_WIDTH__ 1024
#define __SIZEOF_INT1024__ 128
#define __SIZEOF_UINT1024__ 128

На текущей LibMPU 1.0.25 NB_I_MAX == 8192, поэтому family доходит до int65536/uint65536, а __SIZEOF_INT65536__ == 8192.

Decimal-digit metadata определяется для каждой разрешённой integer width:

__INT<bits>_DECIMAL_DIG__
__UINT<bits>_DECIMAL_DIG__

Значение вычисляется собственными integer-only helpers mcpu-cpp из известной ширины типа. Оно означает точное число десятичных цифр максимального значения соответствующего типа: знак и завершающий NUL в DECIMAL_DIG не входят. Для unsigned используется максимум 2^bits - 1, для signed – 2^(bits-1) - 1. Это отличается от LibMPU _int_digs(), которая предназначена для оценки строкового буфера и включает место для завершающего NUL.

Например:

#define __INT64_DECIMAL_DIG__ 19
#define __UINT64_DECIMAL_DIG__ 20
#define __INT256_DECIMAL_DIG__ 77
#define __UINT256_DECIMAL_DIG__ 78

Только сами textual maxima намеренно ограничены шириной bits <= 256:

__INT128_MAX__
__UINT128_MAX__

Максимумы строятся через LibMPU iuitoa(). Макро __INT<bits>_MIN__ не создаются: predefined table не должна содержать вычисляемые выражения вида (-__INT<bits>_MAX__ - 1). Для widths больше 256 бит отсутствуют только MAX; TYPE/WIDTH/SIZEOF/DECIMAL_DIG сохраняются до полного NB_I_MAX * 8.

8.9. Real и Complex families LibMPU

Real/Complex structural metadata генерируется для каждой power-of-two ширины от 32 бит до фактического configured MPU_REAL_IO_LIMIT. Для всех этих типов публикуются TYPE, WIDTH и SIZEOF.

Для Complex WIDTH означает параметр типа, а не суммарную storage width:

#define __COMPLEX128_TYPE__ complex128
#define __COMPLEX128_WIDTH__ 128
#define __SIZEOF_COMPLEX128__ 32

complex128 состоит из двух компонентов real128, поэтому его storage size равен 32 байтам. При MPU_REAL_IO_LIMIT == 65536 верх family имеет вид:

#define __COMPLEX65536_TYPE__ complex65536
#define __COMPLEX65536_WIDTH__ 65536
#define __SIZEOF_COMPLEX65536__ 16384

Для Real соответственно:

#define __REAL65536_TYPE__ real65536
#define __REAL65536_WIDTH__ 65536
#define __SIZEOF_REAL65536__ 8192

Precision metadata определяется для всех разрешённых Real widths вплоть до MPU_REAL_IO_LIMIT. Имена macros согласованы с LibMPU helpers:

__REAL<bits>_DECIMAL_DIG__  -> _real_digs(bits/8)
__REAL<bits>_MANT_DIG__     -> _real_mant_digs(bits/8)

__REAL<bits>_DIG__ намеренно отсутствует. Ограничение bits <= 256 относится только к большим textual numeric constants. Для размеров до 256 бит также определяются:

__REAL<bits>_MAX__
__REAL<bits>_MIN__
__REAL<bits>_EPSILON__
__REAL<bits>_MAX_EXP__
__REAL<bits>_MIN_EXP__
__REAL<bits>_MAX_10_EXP__
__REAL<bits>_MIN_10_EXP__

Например, на LibMPU 1.0.25 для real128 текущий profile даёт значения вида:

#define __REAL128_EPSILON__ 2.524354896707237777317531409e-29
#define __REAL128_MAX__ 4.197157432934775384808581951e+323228496
#define __REAL128_MIN__ 9.530259619551804292864984035e-323228497
#define __REAL128_MAX_10_EXP__ 323228496
#define __REAL128_MAX_EXP__ 1073741823
#define __REAL128_MIN_10_EXP__ -323228524
#define __REAL128_MIN_EXP__ -1073741822

MAX/MIN/EPSILON создаются самой LibMPU и преобразуются через real_to_ascii(). Exponent constants получают значения через LibMPU exponent helpers и integer conversion. Для widths больше 256 бит эти numeric predefines отсутствуют, но TYPE/WIDTH/SIZEOF/DECIMAL_DIG/MANT_DIG продолжаются до MPU_REAL_IO_LIMIT.

Для каждого разрешённого Real type вплоть до MPU_REAL_IO_LIMIT также публикуются две компактные характеристики:

#define __SIZEOF_REAL128_EXP__ 4
#define __REAL128_MAX_STRLEN__ 60

__SIZEOF_REALxxx_EXP__ непосредственно получает _sizeof_exp(NB_Rxxx). __REALxxx_MAX_STRLEN__ получает _real_max_string(NB_Rxxx) и означает максимальное количество символов текстового представления, а не количество байт. Поэтому для zero-terminated строки нужно резервировать не менее __REALxxx_MAX_STRLEN__ + 1 элементов: для char8 это столько же bytes, а для char16 физический объём в bytes вдвое больше. Эти два metadata-macro определяются и для Real widths больше 256, поскольку сами их значения малы.

8.10. Dump macros: -dM, -dMP

Опция:

mcpu-cpp -dM input.c

печатает только итоговые непредопределённые macros в форме #define .... К этой группе относятся определения из основного файла и включённых headers, а также определения командной строки -D. Предопределённые macros самого MCPU-CPP в -dM не выводятся. Поэтому -dM предназначен прежде всего для короткой инспекции macro-state, созданного пользовательской программой.

Опция:

mcpu-cpp -dMP input.c

добавляет к тому же итоговому состоянию активные predefined macros MCPU-CPP. Вывод имеет две последовательные группы: сначала все predefined macros, затем все непредопределённые macros. Внутри каждой группы определения детерминированно сортируются по имени. Такое разделение удобно системному разработчику для инспекции preprocessing ABI и архитектурных свойств текущей MCPU environment, не смешивая их с пользовательскими определениями.

Принадлежность к группе определяется происхождением macro, а не его именем. Macro, заданный через -D или #define, является обычным даже если его имя похоже на системное. Если predefined macro был удалён через #undef, он не печатается. Если после этого то же имя снова определено пользователем, новое определение относится к обычной группе и выводится в её части -dMP, а также в -dM. Тем самым оба режима показывают именно итоговый macro-state.

Context-dependent __FILE__, __LINE__, __DATE__, __TIME__, __BASE_FILE__ и __INCLUDE_LEVEL__ в статическом dump не печатаются. Статические ABI/architecture predefined macros и вычисляемые static Real metadata выводятся в -dMP.

Если input file указан, он сначала полностью препроцессируется, после чего выводится итоговый macro-state; обычный preprocessed text в режимах -dM и -dMP не выдаётся. Без input file используется stdin, поэтому пустой stdin с -dM даёт пустой dump, а -dMP позволяет получить набор активных static predefined macros текущей MCPU environment.

-dD имеет другую семантику и этим разделением не затрагивается.

8.11. Dump definitions: -dD

Опция:

mcpu-cpp -dD input.c

сохраняет обычный результат препроцессирования и одновременно выводит встреченные директивы #define. Перед началом основного входного текста печатаются статические предопределённые macro definitions. Каждой такой дефиниции предшествует marker:

# 0 "<built-in>"
#define NAME value

а перед блоком предопределённых macro выводится marker исходного файла вида # 0 "input.c". Context-dependent __FILE__, __LINE__, __DATE__, __TIME__, __BASE_FILE__ и __INCLUDE_LEVEL__ в начальный built-in block не включаются.

8.12. Dump configuration: -dconfig

Опция:

mcpu-cpp -dconfig

не требует input file и выводит effective variables configuration layer после чтения runtime config, необязательного system override, домашнего user override или выбранного --config-file, включая expansion $NAME/${NAME}. Строки сортируются по имени и печатаются в форме:

NAME = value;

Это позволяет проверить реальные include paths без ручного поиска <runtime-root>/etc/mcpu-cpp.conf, /etc/mcpu/mcpu-cpp.conf и $HOME/.mcpu/mcpu-cpp.conf.

8.13. Verbose configuration snapshot: -v

При -v MCPU-CPP сохраняет прежний runtime trace для #lang, #include и #include_next, но конфигурационные переменные печатаются только один раз – после чтения всех уровней configuration и применения правил приоритета. Поэтому в verbose output видны только effective values, а промежуточные значения из runtime-root, system и user config не дублируются.

Config-блок выводится в порядке include policy: language-specific user paths, общий user path, system root и AFTER path. Переменная, отсутствующая во всех уровнях configuration, не печатается. Runtime-derived default MCPU_CPP_SYSTEM_INCLUDE_PATH является полноценным самым нижним значением и поэтому виден при -v, даже если ни один mcpu-cpp.conf не найден или все config-файлы отключены опцией --no-config.

Форма строки:

config: NAME=value

8.14. Effective search directories: -dsearch-dirs

Опция:

mcpu-cpp -dsearch-dirs

не требует input file, печатает effective глобальные каталоги поиска и завершает работу без preprocessing. Формат намеренно прост:

search: /path/to/directory

Каталоги выводятся в семантическом порядке классов поиска:

explicit -I
explicit -isystem
configured language-specific user directories
MCPU_CPP_INCLUDE_PATH
MCPU_CPP_SYSTEM_INCLUDE_PATH/<lang>
MCPU_CPP_SYSTEM_INCLUDE_PATH
explicit -idirafter
MCPU_CPP_AFTER_INCLUDE_PATH

Language-specific entries печатаются для всех поддерживаемых языков в их каноническом порядке. Во время реального #include из этой группы участвует только каталог активного #lang. Каталог текущего физического файла в -dsearch-dirs не выводится: он существует только динамически для конкретного #include "..." и меняется вместе с include stack. --no-config не удаляет runtime-derived system root, поэтому без конфигурационных файлов dump всё равно содержит <runtime-root>/include/<lang> и <runtime-root>/include. -nostdinc удаляет из dump effective system <lang> entries и system root, но не explicit -isystem. Не существующий на filesystem каталог всё равно показывается, поскольку он является элементом effective search configuration и просто будет пропущен при реальном поиске файла.

-dsearch-dirs учитывает -I, -isystem, -idirafter, все уровни config и replacement-семантику MCPU_CPP_SYSTEM_INCLUDE_PATH. Опция -o вместе с ним является ошибкой.

8.15. Условная компиляция

Директивы #if, #ifdef, #ifndef, #elif, #else и #endif обрабатываются как управляющие директивы препроцессора и в выходной поток не копируются, в том числе при -dD. Неактивные ветви пропускаются без выполнения находящихся в них #define, #undef и #include; вложенные условные группы при этом учитываются корректно.

Выражение #if сначала обрабатывает оператор defined, затем выполняется macro expansion, а оставшиеся идентификаторы имеют значение 0. Поддерживаются арифметические, битовые, сравнительные и логические операции, ?: и short-circuit semantics для &&, || и ?:.

Начиная с 0.0.26 синтаксис выражения разбирается parser-ом, генерируемым ZUBR 4.1.0 из src/mcpp-expr.zubr; в том же файле находится UCS-2 lexical analyzer. Предварительная обработка defined и macro expansion выполняются до входа в parser. Арифметическая семантика вынесена в mcpp-semantic.c/h и не зависит от размеров целых типов host-системы. Generated mcpp-expr.c включается в release, поэтому ZUBR требуется только при изменении grammar.

8.15.1. Единственная вычислительная разрядность – 64 бита

MCPU-CPP является препроцессором, а не компилятором языка общего назначения. Все целочисленные вычисления в директивах условной компиляции выполняются только в 64-разрядной арифметике. Препроцессор не выполняет арифметику LibMPU произвольной разрядности, вещественные или комплексные вычисления.

Если программисту не требуется управлять двоичным представлением литерала, достаточно обычных целых констант и необязательного U/u. Например:

#if 2 > 1
#if 0xffffffffffffffffU > 1

Числовой lexeme хранится в UCS-2 до классификации, после чего его ASCII-часть передаётся LibMPU iatoui(). Поддерживаются 0b..., 0..., decimal и 0x.... Значение, не помещающееся в 64 бита, является ошибкой. Старые C suffixes L, l, LL, ll не поддерживаются.

8.15.2. Суффикс разрядности zNNN[Uu]

MCPU-CPP понимает общий для MCPU-языков суффикс разрядности:

zNNN
ZNNN
zNNNu
zNNNU
ZNNNu
ZNNNU

NNN – непустая последовательность десятичных цифр и всегда читается как десятичное число, даже если начинается с нулей. Поэтому z8, z08 и z008 задают одну и ту же разрядность 8 бит.

В общем синтаксисе MCPU корректная разрядность должна быть степенью двойки от 8 до MPU_REAL_IO_LIMIT. MCPU-CPP, однако, сознательно ограничен 64-битными вычислениями:

  • z8, z16, z32, z64 и варианты регистра допустимы;
  • значение NNN > 64 немедленно является ошибкой: препроцессор не допускает числовые константы разрядности выше 64 бит в директивах условной компиляции;
  • если NNN <= 64, но не задаёт допустимую степень двойки, например z24, выводится warning и сам zNNN игнорируется;
  • необязательный следующий U/u задаёт unsigned и сохраняет своё значение даже если некорректный zNNN был проигнорирован.

После полного суффикса должна заканчиваться числовая preprocessing token. Оператор или punctuation начинает следующий token, поэтому допустимы 1z32u+2, (1z32u) и 1z32u==1. Записи вроде 1z32undefined, 1z32ufoo и 1z32$foo являются ошибками и не разбиваются искусственно на число и имя.

8.15.3. Нормализация литерала

Суффикс разрядности действует только один раз – при формировании значения самой константы. Разрядность не сохраняется в semantic value и не участвует в последующих операциях.

Для VALUEzNNN значение считается знаковым N-битным числом в дополнительном коде:

  1. сохраняются младшие NNN бит;
  2. результат расширяется со знаком до 64 бит.

Для VALUEzNNNu/VALUEzNNNU сохраняются младшие NNN бит, после чего выполняется нулевое расширение до 64 бит.

Например:

0x7fz8    -> 0x000000000000007f  ->  127
0x80z8    -> 0xffffffffffffff80  -> -128
0xffz8    -> 0xffffffffffffffff  ->   -1
0x80z8u   -> 0x0000000000000080  ->  128
0xffz8u   -> 0x00000000000000ff  ->  255
0x1ffz8   -> 0xffffffffffffffff  ->   -1
0x1ffz8u  -> 0x00000000000000ff  ->  255

Последние два примера намеренны: zNNN задаёт разрядность двоичного представления, а не проверку математического диапазона. Биты старше N отбрасываются до расширения.

После этой нормализации никакой z8, z16 или z32 в вычислительной модели уже не существует. Внутреннее значение содержит только 64-битный битовый образ и признак signed/unsigned.

8.15.4. Все последующие операции – 64-битные

После нормализации все арифметические, побитовые, сравнительные и логические операции выполняются над 64-битными операндами. Результат операции не усекается обратно до разрядности исходного suffix. Поэтому:

0x7fz8 + 1   -> 128
0xffz8u + 1  -> 256

а не -128 и 0 соответственно. Аналогично ~0xffz8u инвертирует все 64 бита и даёт 0xffffffffffffff00.

Для бинарных операций, где signedness имеет значение, наличие unsigned операнда переводит операцию в 64-битную unsigned-интерпретацию. Сравнения возвращают 0 или 1. Логические !, &&, || также возвращают signed 64-битные 0 или 1; short-circuit не вычисляет невыбранную часть.

Сдвиги выполняются после 64-битной нормализации. Правый сдвиг signed отрицательного значения является арифметическим, unsigned – логическим. Например:

0x80z8  >> 1  -> -64
0x80z8u >> 1  ->  64

Историческое правило MCPU-CPP для отрицательного счётчика сдвига сохраняется: A << -N эквивалентно A >> N, а A >> -N – A << N.

Таким образом, zNNN не превращает препроцессор в компилятор с системой integer promotions разных размеров. Он лишь позволяет явно описать битовый образ исходного литерала; затем выражение вычисляется в единственной простой 64-битной модели.

8.15.5. Символьные константы

Символьная единица имеет тип __mpu_uint16_t, соответствующий внутреннему UCS-2 представлению, и перед вычислением расширяется нулями до 64 бит. Последующая арифметика снова является обычной 64-битной арифметикой.

Состояние условной компиляции хранится в отдельном стеке; условная группа не может пересекать границу include-файла.

8.16. Диагностические директивы #error и #warning

MCPU-CPP поддерживает стандартные диагностические директивы:

#error сообщение
#warning сообщение

#error выдаёт diagnostic уровня error с текущими логическими именем файла и номером строки и немедленно завершает preprocessing с ошибкой. #warning выдаёт warning с той же source-location information, после чего preprocessing продолжается. Поэтому предшествующий #line влияет на координаты обеих диагностик.

Остаток строки после имени директивы не подвергается macro expansion. Например:

#define MESSAGE expanded
#warning MESSAGE

печатает MESSAGE, а не expanded. Это отличает диагностические директивы от #if и #line, где macro expansion является частью соответствующего контракта.

Комментарии удаляются на обычной preprocessing phase до обработки директивы. Начальные и конечные пробелы сообщения удаляются, последовательности пробельных символов между preprocessing tokens сворачиваются в один пробел. Пробелы внутри кавычек сохраняются. Например:

#warning   one   /* comment */   two
#warning "a   b"

дают сообщения соответственно one two и "a b". Unicode-текст проходит через внутреннее UCS-2 представление и выводится во внешнюю диагностику в UTF-8.

Обе директивы являются управляющими и никогда не копируются в обычный выходной поток или в -dD. В неактивной ветви #if они полностью игнорируются, поэтому обычная защитная конструкция работает ожидаемо:

#if 0
#error this error is inactive
#endif

8.17. Управление предупреждениями: -Wcomment, -Wall, -Werror

MCPU-CPP разделяет обязательные предупреждения, являющиеся частью уже зафиксированной preprocessing-семантики, и дополнительные классы предупреждений, которые включаются пользователем. Управление предупреждениями не изменяет семантику -dD, macro expansion, conditional compilation или include search.

Опции -Wcomment и -Wcomments являются полными синонимами и включают два лексических предупреждения:

  • последовательность /*, встретившуюся внутри уже открытого /* ... */ комментария;
  • backslash-newline внутри // комментария, из-за которого однострочный комментарий физически продолжается на следующую строку.

По умолчанию этот дополнительный класс выключен. -Wall включает все дополнительные warning classes MCPU-CPP; в версии 0.0.40 таким классом является -Wcomment. Формы -Wno-comment и -Wno-comments выключают его. Как в GNU warning model, более специфическая настройка имеет приоритет над групповой независимо от порядка аргументов. Поэтому обе команды:

mcpu-cpp -Wall -Wno-comment file.c
mcpu-cpp -Wno-comment -Wall file.c

оставляют comment warnings выключенными. Между настройками одинаковой специфичности действует последнее указание, например -Wno-comment -Wcomment включает этот класс.

-Werror не включает никаких новых warning classes. Он повышает до error любое предупреждение, которое в данном запуске действительно было бы выдано, и такой запуск завершается неуспешно. Это относится как к дополнительным comment warnings, так и к уже существующим обязательным предупреждениям MCPU-CPP:

  • активной директиве #warning;
  • недопустимой, но не превышающей 64 бита ширине zNNN;
  • переопределению macro другим replacement list;
  • результату ##, не образующему один preprocessing token.

Например:

mcpu-cpp -Wcomment -Werror file.c

превращает найденный comment warning в error. В то же время один -Werror без -Wcomment/-Wall не заставляет MCPU-CPP искать optional comment warnings.

-Wno-error возвращает обычную severity warning. Для -Werror и -Wno-error, имеющих одинаковую специфичность, действует последняя опция командной строки. Так, -Werror -Wno-error оставляет warnings предупреждениями, а -Wno-error -Werror снова повышает их до errors.

В 0.0.40 намеренно не вводятся -Werror=<class>, -Wno-error=<class>, -Wundef, -Wunused-macros, -Wtraditional и другие компиляторные классы. Warning interface MCPU-CPP остаётся компактным и расширяется только тогда, когда новый класс действительно нужен самому preprocessing language.

8.18. Идентификаторы UCS-2

Начиная с 0.0.22 имена preprocessing identifiers больше не ограничены ASCII. Внутри mcpu-cpp текст уже представлен строгим UCS-2, а классификация символов выполняется locale-independent функциями LibMPUIO 1.0.4, построенными по Unicode 18.0.0. Первый символ идентификатора должен быть _ или иметь свойство XID_Start; последующие символы должны быть _, $ или иметь свойство XID_Continue. Символ $ является расширением mcpu-cpp: он разрешён только после первого символа и не может начинать identifier. Это правило едино для имён и параметров macro, #undef, #ifdef/ #ifndef, defined, обычного macro expansion, #/##. Имена остаются case-sensitive. Surrogate code units U+D800..U+DFFF не являются допустимыми символами identifiers.

Например, допустимы:

#define АНДРЕЙ 1
#define résumé 2
#define ΩМЕГА 3
#define VALUE$OLD 4

Например, VALUE$OLD допустим, а $VALUE недопустим, поскольку $ не является identifier-start character.

Combining marks и не-ASCII decimal digits могут входить в identifier в позициях XID_Continue, но не становятся автоматически допустимыми первыми символами. Синтаксис числовых констант от этого не меняется: его правила остаются правилами соответствующего языка, а не Unicode isdigit.

8.19. Макросы командной строки -D и -U

Начиная с 0.0.23 опции -D и -U являются полноценными действиями препроцессора. Поддерживаются формы:

-DNAME
-DNAME=VALUE
-D'FUNC(a,b)=a+b'
-UNAME

-DNAME эквивалентна #define NAME 1; наличие = с пустой правой частью задаёт пустой replacement list. Function-like определения используют тот же macro engine, что и обычный #define, включая параметры, #, ## и последующий rescanning. -U использует тот же identifier contract, что и #undef. Действия -D/-U выполняются в порядке командной строки после установки predefined macros.

Только payload опций -D и -U интерпретируется как UTF-8 и преобразуется в строгий UCS-2. Имена файлов, -I, другие pathname arguments и остальные аргументы командной строки остаются исходными byte strings и не подвергаются Unicode-конвертации.

Начиная с 0.0.25 символ $ разрешён внутри имени macro, но не в первой позиции. При передаче $ из shell пользователь обязан учитывать правила самого shell: shell обрабатывает $ до запуска mcpu-cpp . Одинарные кавычки уже полностью защищают $, например:

mcpu-cpp '-DАНДРЕЙ$_Y=62' input.c

Без кавычек $ следует экранировать:

mcpu-cpp -DАНДРЕЙ\$_Y=62 input.c

или использовать двойные кавычки с экранированием:

mcpu-cpp -D"АНДРЕЙ\$_Y=62" input.c

Вариант без защиты:

mcpu-cpp -DАНДРЕЙ$_Y=62 input.c

не передаёт написанное имя буквально: $... сначала раскрывается shell и mcpu-cpp получает уже изменённый argv. Внутри одинарных кавычек обратная косая черта перед $ не нужна и стала бы обычным символом аргумента.

Для command-line -D левая часть до первого = разбирается как отдельный macro declarator. Если после допустимого имени (или завершённого списка параметров function-like macro) до = встречается недопустимый хвост, этот хвост молча отбрасывается и никогда не превращается в replacement list. Например:

-D'АНДРЕЙ@XYZ=62'

эквивалентно:

#define АНДРЕЙ 62

а не ошибочной форме #define АНДРЕЙ @XYZ 62. Аналогичное правило допустимого identifier-prefix применяется к -U. Если же первый символ вообще не является допустимым identifier-start character (например $ или цифра), определение остаётся ошибочным.

При -dD определения, пришедшие через -D, маркируются отдельно от предопределённых macro:

# 0 "<command-line>"
#define NAME value

в то время как predefined macros продолжают использовать <built-in>.

8.20. Публичный интерфейс командной строки

mcpu-cpp поддерживает только актуальные опции, описанные --help. Устаревшие compatibility-флаги не образуют скрытый интерфейс и диагностируются как unknown option. Опция -E является исключением: она молча принимается и игнорируется, поскольку может передаваться compiler driver при запуске отдельного препроцессора.

Опция --object-suffix SUFFIX задаёт суффикс object target, используемый при генерации make-зависимостей; аргумент обязателен.

9. Build-system и генераторы

Собственные Autoconf-макросы проекта находятся в корневом acsite.m4. Каталог m4/ зарезервирован для внешних/vendor M4-файлов. Такой порядок повторяет принятую в библиотеках MCPU схему и не смешивает собственный configure-код с импортированными макросами.

Парсер выражений #if генерируется ZUBR 4.1.0 из src/mcpp-expr.zubr. Release archive содержит и грамматику, и уже сгенерированный src/mcpp-expr.c, поэтому обычная сборка не требует установленного ZUBR. После изменения грамматики developer build использует штатное правило Automake:

zubr -vl -s -Bmcpp_ -o mcpp-expr.c mcpp-expr.zubr

Перед выпуском release generated C должен соответствовать грамматике, полный test suite и make distcheck должны проходить без ошибок.

9.1. Developer bootstrap и Git source tree

Начиная с 0.0.50 корневой скрипт ./bootstrap позволяет не хранить в Git файлы, которые полностью воспроизводятся из исходников. Скрипт сначала генерирует src/mcpp-expr.c из src/mcpp-expr.zubr с помощью ZUBR 4.1.0, затем выполняет aclocal, autoheader, automake и autoconf в стиле библиотек LibMPU и LibMPUIO. Опция --target-dest-dir=DIR задаёт target ROOTFS для системных Autoconf macro/include directories.

Это правило относится именно к developer Git tree. Release archive остаётся самодостаточным, как и раньше: он содержит configure, Makefile.in, helper scripts Automake и уже сгенерированный src/mcpp-expr.c, поэтому обычная сборка релиза не требует предварительного запуска bootstrap и не требует ZUBR.

Корневой .gitignore перечисляет воспроизводимые bootstrap-файлы и обычный configure/build state. Он не меняет существующую release/build model, а только позволяет поддерживать более чистый Git repository.

10. GNU-compatible features

mcpu-cpp является самостоятельным препроцессором MCPU, но для ряда хорошо известных операций намеренно повторяет поведение GNU CPP. Совместимость относится к документированным возможностям, а не означает полную CLI- или языковую взаимозаменяемость с GCC.

В частности, GNU-compatible поведение используется для:

  • object-like и function-like macro, повторного macro rescan, # и ##;
  • variadic macro ... / __VA_ARGS__ и стандартного __VA_OPT__;
  • #if, #ifdef, #ifndef, #elif, #else, #endif и defined;
  • #include, #include_next, #pragma once, #line и GNU linemarkers;
  • compact output mapping: до семи невидимых строк представляются newline, а разрыв в восемь и более строк – корректирующим linemarker;
  • forced files -include / -imacros и dependency options -M, -MM, -MD, -MMD, -MF, -MT, -MQ, -MG;
  • warning controls -w, -Wall, -Werror и поддерживаемых -Wcomment forms.

MCPU-specific возможности, включая #lang / #endlang, числовой суффикс zNNN и ABI predefined macros, остаются собственными расширениями mcpu-cpp.

Исходный код и выпуски

Текущий публичный выпуск: 1.0.3.