Фреймворк для процедурных макросов в Rust

в 17:27, , рубрики: Rust, Компиляторы, макросы, Программирование

От переводчика

Процедурные макросы — одна из наиболее ожидаемых фич Rust. На данный момент процедурные макросы возможно писать только под нестабильную версию компилятора, хотя есть несколько контейнеров, вроде syntex, позволяющие делать ограниченную кодогенерацию в рамках стабильного компилятора. Однако ситуацию это особо не облегчает, поскольку интерфейс к AST остаётся нестабильным, и, хотя авторы syntex стараются идти в ногу с ночными сборками, иногда случаются фейлы из-за изменений в структуре AST.
В этом блог посте один из участников core team — Nick Cameron — поделился своим видением будущего процедурных макросов. Хотя пост полон технических подробностей по внутренностям компилятора, мне показалось, что хабрасообществу может быть интересно заглянуть немного за кулисы разработки Rust.

Фреймворк для процедурных макросов

В этом посте я расскажу, как, по моему мнению, должны выглядеть процедурные макросы. Я уже рассказывал про синтаксис в другом посте, а когда мы опубликуем API для процедурных макросов, то напишу пост и про него. Я уже описывал целый ряд изменений в системе макросов, так что здесь я в чём-то повторюсь (отчасти противореча прошлому посту), но раскрою больше подробностей.

Виды макросов

Есть всего два вида процедурных макросов: макросы-функции и макросы-атрибуты. Первые — функции помеченные атрибутом #[macro], вторые — помеченные #[macro_attribute]. Макросы-функции используются в виде foo!(tokens), а макросы-атрибуты в виде #[foo] или #[foo(tokens)], при этом подсоединяясь к узлу AST, следуя обычным для Rust правилам использования атрибутов. Атрибуты #![...] тоже поддерживаются, в соответствии с очевидной семантикой (они относятся не к следующем за ними узлу AST, а к родительсткому — прим. перев.).
Макросы-функции имеют следующую сигнатуру:

#[macro]
pub fn foo(TokenStream, &mut MacroContext) -> TokenStream

Макросы-атрибуты имеют такую сигнатуру:

#[macro_attribute]
pub fn foo(Option<TokenStream>, TokenStream, &mut MacroContext) -> TokenStream

Первый аргумент — опциональный поток токенов из самого макро-атрибута (tokens из #[foo(tokens)]). Второй аргумент — TokenStream — поток токенов из узла AST, к которой макро-атрибут относится. Возвращаемый TokenStream заменяет оригинальный узел AST, и может представлять собой ноль или больше AST узлов (то есть мы заменяем синтаксические расширения Modifier и Decorator разом).
Мы гарантируем, что второй TokenStream парсится в какой-то валидный узел AST, в то время как первый может парситься, а может и нет.
Процедурный макрос должен убедиться, что возвращаемый TokenStream парсится в контексте вызова макроса.

libmacro

Libmacro — новая библиотека, которая добавляется в стандартную поставку языка. Предполагается, что она будет использоваться в основном процедурными макросами. Её содержимое будет следовать тем же правилам стабилизации, что и остальные библиотечные контейнеры (то есть все фичи вводятся как нестабильные, а затем стабилизируются по мере того, как их полезность доказывается). Библиотека libsyntax остаётся, но будет деталью реализации компилятора: процедурные макросы не должны использовать её и она не будет помечена как стабильная (то есть стабильные макросы не должны её использовать).
Идея в том, что libmacro предоставит достаточно низкоуровневый интерфейс. Мы ожидаем, что в экосистеме появятся контейнеры с более высокоуровневыми библиотеками. В частности libmacro не будет иметь концепции AST. Ожидается, что контейнеры в более широкой экосистеме предоставят AST, как и функциональность для парсинга токенов в AST, и построение AST.
Libmacro будет содержать структуры для токенов (которые могут быть реэкспортированы из libsyntax) и MacroContext, передаваемый в макросы. Libmacro будет включать следующую функциональность:

  • разбор строки на токены,
  • квази-цитирование (преобразование текста и мета-переменных в токены),
  • сопоставление с образцом для токенов,
  • интернирование строк,
  • генерация новых идентификаторов с различными настройками гигиены,
  • манипулирование информацией о гигиене токенов,
  • применение макросов (включая разрешение имён),
  • манипулирование спанами («spans» — представление отображения кусков AST на исходный код, не знаю как лучше перевести — прим. перев.) (в частности раскрытие трейсов и создание новых спанов) и получение информации о месте в исходном коде из спанов,
  • проверка состояния флагов фич («feature gates», атрибутов вида #[feature(name)], включающих различные, обычно нестабильные, возможности компилятора — прим. перев.) и установка флагов фич для использования во время кодогенерации,
  • пометка атрибутов как используемых,
  • вывод сообщений об ошибках и т.д.,
  • парсинг токенов в пары ключ-значение, как указано в аргументах атрибута.

Большая часть этого функционала будет доступна в виде методов MacroContext.
Я расскажу про это API более подробно в будущем посте. Здесь я раскрою некоторые аспекты токенов и MacroContext.

Токены

Создание эффективного и эргономичного представления токенов затрагивает множество сфер. Вот первый набросок:

mod tokens {
    use {Span, HygieneObject, InternedString};

    pub struct TokenStream(Vec<TokenTree>);

    impl TokenStream {
	// Methods for adding and removing tokens, etc.
    }
    
    pub struct TokenTree {
	pub kind: TokenKind,
	pub span: Span,
	pub hygiene: HygieneObject,
    }
    
    pub enum TokenKind {
	Delimited(Delimiter, Vec<TokenTree>),
    
	// String includes the commenting tokens.
	Comment(String, CommentKind),
	String(String, StringKind),
    
	Dollar,
	Semicolon,
	Eof,
    
	Word(InternedString),
	Punctuation(char),
    }
    
    pub enum Delimiter {
	// { }
	Brace,
	// ( )
	Parenthesis,
	// [ ]
	Bracket,
    }
    
    pub enum CommentKind {
	Regular,
	InnerDoc,
	OuterDoc,
    }
    
    pub enum StringKind {
	Regular,
	Raw(usize),
	Byte,
	RawByte(usize),
    }
}

Мы могли бы хранить HygieneInformation только для TokenKind::Word, а не для всех токенов. Мы так же могли бы хранить её для диапазонов токенов, а не для каждого токена отдельно.
Я не уверен, что нам нужно различать $ и ;: доллар используется для обозначения метапеременных в макросах, а точка с запятой — для отделения элементов друг от друга, так что возможно будет полезно из различать. Возможно нам стоит различать ! и #, так как они используются при вызове макросов, хотя я и не могу придумать, где это может пригодиться.
Возможно стоит интернировать строковые литералы. Возможно не стоит сохранять содержимое комментариев, так как их можно прочитать через спаны (сейчас мы делаем и так, и так).
Я не думаю, что нам здесь нужны интерполированные не-терминалы.
Нам следует так же предоставить некоторые вспомогательные функции. Однако заметьте: я ожидаю, что мы со временем предоставим гарантии стабильности для этих структур данных. Эти функции будут стабильны только по сигнатурам, но не по результатам работы. Они будут принимать либо TokenTree, либо &[TokenTree]:

  • is_keyword
  • is_reserved_word
  • is_special_ident
  • is_operator
  • is_ident
  • is_path
  • metavariables — вычленяет метапеременные из TokenStream, например для foo($x:ident, $y:expr) вернёт [("x", 2 ident), ("y", 6, expr)] в виде некой структуры данных.

И возможно, некоторые функции для удобства построения дерева токенов.

MacroContext

MacroContext выполняет несколько ролей:

  • содержит информацию о контексте объявления макроса и контексте его применения,
  • передаёт информацию о том, как должны быть использованы результаты работы макроса,
  • предоставляет доступ к функциональности libmacro, которая требует сохранения некоторого состояния.

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

Контекстная информация

Методы доступа:

  • спан использования макроса (обратите внимание: TokenStream аргументов макроса тоже имеет свой собственный спан),
  • спан определения самого макроса,
  • гигиенический контекст места использования макроса, и места определения макроса (заметьте, что это закрытие объекты, опять же, все токены будут иметь свои собственные гигиенические контексты),
  • любые не развёрнутые атрибуты на месте применения макроса,
  • вид AST узла, который макрос должен произвести,
  • флаги фич, включённые по месту использования макроса,
  • информация о том, используется ли макрос в unsafe-блоке или нет,
  • разделители, используемые для функции-макроса.
Свойства возвращаемых токенов

  • набор флагов фич для сгенерированного кода,
  • указание того, как применять гигиену в сгенерированном коде.
Прочая функциональность

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

Ввод в эксплуатацию

Изначально мы будем поддерживать и новые процедурные макросы, и старые расширения синтаксиса. Оба будут нестабильными. Определение старых расширений синтаксиса должны будут выдавать предупреждения об устаревании (deprecation) с предложением использовать новые процедурные макросы. Мы будем стабилизировать процедурные макросы со временем, через стабилизацию атрибутов для объявления процедурных макросов. Затем, постепенно стабилизируем libmacro часть за часть. Как только достаточная часть функциональности станет стабильной (а мы перепишем внутренние расширения синтаксиса на новую систему), мы должны будем удалить поддержку старых расширений синтаксиса.

Альтернативы

Сейчас мы поддерживаем синтаксическое расширение IdentTT, которое представляет функцию-макрос с идентификатором между именем макроса и отрывающим разделителем. Я бы хотел прекратить эту поддержку. Однако может оказаться полезным эмулировать некоторые элементы (например my_struct! foo { ... }). К сожалению такое применение неудовлетворительно, так как не поддерживает модификаторы (вроде pub my_struct! foo ...), а некоторые авторы хотят различные виды токенов после имени макроса при его вызове, а не только идентификаторы. Моё предложение таково, что нам стоит убрать данную возможность на данный момент. Её можно будет добавить в будущем с сохранением обратной совместимости, либо через добавление нового атрибута (#[macro_with_ident]), либо через добавления информации в MacroContext.
MacroContext несколько тяжеловесен, возможно лучше было бы разделить его на несколько меньших типажей или структур. Однако это, возможно, сделает написание макросов менее эргономичным.

Автор: kstep

Источник

Поделиться новостью

* - обязательные к заполнению поля