Принялось решение добавить регулярные выражения в свой язык программирования. По началу я подумал, что мне совершенно незачем в них разбираться и в интернете, наверняка, уже есть полно готовых библиотек. Стал искать, нашёл какие-то осколки кода на С++, которые ничего не дают. Пришлось самому разобраться, что такое регулярные выражения тут. Ради спортивного интереса, я решил сделать свою библиотеку на С++.

Стал делать и подумал, а почему бы мне не добавить туда своих тараканов. Я решил добавить две конструкции:

{namesubexpression} - вызов подвыражения по имени "namesubexpression",
($namesubexpression:BodyExpression) - описание под выражения с именем "namesubexpression".

Само описание под выражения может встречаться в любом месте структуры регулярного выражения и игнорируется при поиске, подобно закоментированым: (#MeComment).
Сразу же возникает проблема бесконечной рекурсии.
Вот пример рекурсивного регулярного выражения, которое недопустимо: ($E:{E}){E}

Конечно, я сделал стадию валидации и такие поисковые конструкции просто не допустятся в поисковую машину. Также валидацию не пройдет выражение, которое содержит в себе вызов неописанного подвыражения.

Вот пример текста, который можно спарсить рекурсивным регулярным выражением (РРВ): [[[[[A]]]]]
А вот его РРВ: ($RRE:\[({RRE}|A)\]){RRE}

Я также решил добавить три зарезервированные конструкции:
{:String} соответствует выражению: (("(\\.|[^"])*")|('(\\.|[^'])*'))
{:Number} соответствует выражению: (-?[0-9]+\.?[0-9]*[Ee]?-?[0-9]*)
{:Name} соответствует выражению: ([A-Za-z][A-Za-z0-9]*)
Но их поисковая система не использует структурные элементы аналогичных выражений, а организованна встроенным машинным поиском, который работает значительно быстрее и возвращает одну целую строку текста, в которой содержится всё тело найденного соответствия а не части для каждого компонента в аналогичных регулярных выражениях.

Формат ответа:

Поисковая машина в случае обнаружения хотя бы одного соответствия выдаёт ответ. А именно структуру, которая является множеством элементов, каждый из которых может быть либо строкой текста, либо таким же множеством. Ответом успешного поиска гарантированно будет множество. Каждым элементом которого будет множество, которое гарантированно состоит из двух элементов. На первом месте гарантированно стоит строка текста: либо "Text", либо "Expression". Это признак порции ответа. В случае "Text", на втором месте гарантированно будет строка текста. В другом случае, на втором месте гарантированно будет описание найденных соответствий с элементами РРВ. Всякое найденное соответствие разделяет текст, в котором проводится поиск, на части, которые находятся между...

Та короче, вот пример:

Пример кода на языке Автор:

	Expression = ":";
	Data = "SomePrefixText :: SomeInteriorText : SomeSufixText.";
	Result = Expression.RegularExpression("Parse",Data);
	trace(JSON("stringify",Result));

Ответ:

[
	[ "Text", "SomePrefixText " ],
	[ "Expression", [ ":" ] ],
	[ "Expression", [ ":" ] ],
	[ "Text", " SomeInteriorText " ],
	[ "Expression", [ ":" ] ],
	[ "Text", " SomeSufixText." ]
]

Вот второй пример:

	Expression = "($RRE:\\(({RRE}|A+)\\)){RRE}";
	Data = "((AAAA))";
	Result = Expression.RegularExpression("Parse",Data);
	trace(JSON("stringify",Result));

Ответ:

[
	[
		"Expression",
		[
			[
				"SubExpression:RRE",
				[
					"(",
					[ [ [ "SubExpression:RRE", [ "(", [ [ [ "A", "A", "A", "A" ] ] ], ")" ] ] ] ],
					")"
				]
			]
		]
	]
]

Теоретически, с помощью одного хорошо сделанного РРВ, можно описать синтаксис любого языка программирования, в котором нет предпроцессора.

Так выглядит PHP: ((<\?(php)?.*?\?>)+|(^<\?(php)?.*))
:-)
Но недостатком такого подхода является невозможность отследить синтаксические ошибки.

А вот ещё один пример.
Ради спортивного интереса я создал и протестировал РРВ, которым можно описать любое РРВ и само себя. Полагаю, вам будет интересно взглянуть:

(?x)
($CallSE:\{({:Name}|:(String|Number|Name))\})
($SpecialChar:\\[DSWdsw])
($SpaceChar:([ \f\n\r\t\v]|\\[fnrtv]))
($x:\\[0-9A-Fa-f]{2})
($O:\\[0-7]{3})
($onec:({x}|{O}|\\.|[^\[\]\\\^]))
($ones:({x}|{O}|\\.|[^\[\]\\\^$|?*+\(\){}]))
($Char:\[\^?({SpecialChar}|({onec}-{onec})|{onec})*?-?\])
($Qantifikator:([\?\*\+]|\{[0-9]*(,[0-9]*)?\})[\?\+]?)
($Grupa:\\[1-9])
($QE:\\Q.*?\\E)
($PosInStr:([\^\$]|\\[Bb]))
($PrefixBody:((\#|\?#)|(\${:Name}:)|(\?[ismx-]*:)|(\?=)|(\?!)|(\?<=)|(\?<!)|(\?=)|(\?>)|(\?)))
($SubE:\({BodyE}\))
($BodyE:(
	(\?[ismx-]*)|
	(
		{PrefixBody}?
		(
			({SubE}|{Char}|{CallSE}|{Grupa}|{QE}|{PosInStr}|{SpecialChar}|{SpaceChar}|(\|)|{ones}){Qantifikator}?
		)*
	)
))
{BodyE}

Вы можете взглянуть на код этой библиотеки РРВ на С++ тут.
Кому надо, берите и пользуйтесь.

Помните: делать свой язык программирования - самая неблагодарная робота.
Но программист-пользователь ценится дороже, чем просто пользователь.

Жду ваших комментариев, критики и интересных предложений.

Комментарии (18)


  1. Zenitchik
    21.01.2023 02:35
    +2

    Вот пример рекурсивного регулярного выражения, который недопустим: ($E:{E}){E}

    Т.е. рекурсия всё-таки не реализована?


    1. monstr0518 Автор
      21.01.2023 02:43
      +1

      Реализована. Но допустимо так: ($OK:X{OK}?){OK} и находит оно: XXXXXXXX. А бесконечное вхождение без смещения каретки поиска недопустимо.


      1. Zenitchik
        21.01.2023 04:13

        Понял. Спасибо. А что под капотом? Рекурсивный спуск?


        1. monstr0518 Автор
          21.01.2023 13:10

          Да.


  1. redfraction
    21.01.2023 03:28

    .


  1. codecity
    21.01.2023 03:34
    +2

    Открыть в Visual Studio C++ 6

    Дата релиза - 1998 год. Можно узнать из любопытства, она нормально устанавливается на Win 10?


    1. monstr0518 Автор
      21.01.2023 13:15

      Не знаю. У меня Win 7. Но по идее любой С++ потянет библиотеку.


  1. KanuTaH
    21.01.2023 03:45
    +12

    По началу я подумал, что мне совершенно незачем в них разбираться и в интернете, наверняка, уже есть полно готовых библиотек. Стал искать, нашёл какие-то осколки кода на С++, которые ничего не дают.

    Регулярные выражения есть в std начиная с C++11, уже больше 10 лет тому как.

    Открыть в Visual Studio C++ 6

    А-а, вон оно чо, Михалыч.


  1. pae174
    21.01.2023 08:22
    +2

    {:Digit} соответствует выражению: (-?[0-9]+.?[0-9]*[Ee]?-?[0-9]*)

    При такой регулярке числом будет считаться даже текст "0<E-"

    Ну и кроме того Digit - это отдельная цифра. А число это Number.


    1. monstr0518 Автор
      21.01.2023 13:29

      На машинном уровне это организованно чуточку иначе: https://github.com/monstr518/RegularExpressions/blob/main/RegularExpressions.cpp#L1878

      Но минус действительно возможен лишний. Я могу это легко исправить, если хоть кому-то это нужно.


  1. Ritan
    21.01.2023 14:39
    +3

    Код - это ужас какой-то. Как будто библиотека напрямую из времён актуальной MSVS 6 прибыла.

    using namespace std;
    
    
    typedef vector<string>	V_S;

    В заголовочном файле? Так не делали даже тогда.

    class BaseFinder {
    public:
    	int type;

    Может всё-таки enum, который был ещё в с89?

    bool SimvolFinder::isProbelSimvol(const char c){
    	if(!c)return 1;
    	const char* chars = " \f\n\r\t\v";
    	int i;
    	for(i=0;chars[i];++i)if(c==chars[i])return 1;
    	return 0;
    }

    isspace?

    И ещё, очень похоже, что это всё не будет работать с utf8 вообще никак


  1. agalakhov
    21.01.2023 15:36
    +2

    Грамматики, могущие содержать рекурсию, называются контекстно-свободными. Их теория и способы описания хорошо разработаны. Выражения, подобные регулярным, для них не используют, так как собственно регулярные выражения придуманы для регулярных грамматик, которые по определению не могут содержать рекурсии, и "расширение возможностей" превращает их в жуткий костыль. Для контекстно-свободных грамматик традиционно применяют намного более удобную и легко читаемую форму записи Бэкуса-Наура.


    1. Zenitchik
      21.01.2023 17:32

      Автор, похоже, независимо изобрёл формализм PEG (parsing expression grammar).
      Это способ описания алгоритмов рекурсивного спуска в виде выражений, похожих на регулярные.


      1. 0xd34df00d
        21.01.2023 18:37

        Пеги мощнее, чем КС.


        1. Zenitchik
          21.01.2023 20:32

          Это можно строго доказать?


          1. 0xd34df00d
            22.01.2023 10:43

            Смотря что конкретно вы понимаете под пегами (и мне стоило это сразу уточнить).


            Если формализм из статьи Форда 20-летней давности — ЕМНИП (но это неточно) там можно парсить a^n b^n c^n, а это каноничный пример не-КС-грамматики. При этом вопрос о том, любая ли КС выразима в том конкретном формализме, насколько я знаю, открыт.


            Если (и это привычная мне интерпретация) вы понимаете любые парсеры на комбинаторах а-ля parsec — да, строго мощнее. Там и КЗ, и КС выражаются явно.


    1. 0xd34df00d
      21.01.2023 18:37

      Насчёт костыльности можно поспорить. Если делать правильно, то там все довольно изящно.


    1. monstr0518 Автор
      23.01.2023 01:36
      -1

      Вы мне ещё раз такое скажете, и я свой ScriptX опубликую. Это язык который со старта знает только описание конструкции БеконаСаируса (так чё-то). И он дополняет свой синтаксис, прямо в процессе исполнения кода :).