В дебрях эзотерики

Сегодня мы с вами погрузимся в мир непризнанного, в практическом смысле, искусства, в самую глубину тайного знания в области программирования — мы познакомимся с эзотерическим языком программирования.

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

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

Один из таких языков программирования называется FALSE, и именно с него началась мода на эзотерические языки программирования. И именно с ним я хотел бы вас познакомить.

Язык программирования FALSE был создан в 1993 году Ваутером ван Ортмерссеном для достижения двух целей: разработка компилятора современного Тьюринг-полного языка программирования, который был бы меньше 1 килобайта и создание синтаксиса, который выглядел бы шифровкой или случайным набором символов для неподготовленного человека.

Что ж, цели были достигнуты — на сайте доктора ван Ортмерссена доступна реализация FALSE с очень интересными примерами и документацией. Я скачал данную реализацию, но компилятор cc в моей операционной системе напрочь отказался компилировать портативную версию компилятора, написанную на C, а ставить иной компилятор и среду разработки для компиляции небольшого «игрушечного» проекта было бы слишком…

Что делать?

Поискав другие реализации FALSE, я не нашел ничего стоящего, кроме интерпретатора в браузере, но пользоваться браузером не всегда удобно, да и операции с файлами с таким интерпретатором не реализуешь…

В общем, именно так я и пришел к идее написания своего интерпретатора. Но после нескольких дней бесплодных попыток самостоятельной реализации, я понял, что нужна какая-то концепция по обработке кода на FALSE — и в этом мне опять помогла стороняя реализация! И она была на Scala — языке, который я не знаю, но тем не менее, я смог перевести код из Scala в D, а также исправить некоторые ошибки автора реализации и добавить некоторые новые возможности в FALSE.

Прежде чем разбираться с реализацией на D, вернемся к самому FALSE.

Язык программирования FALSE помимо эзотеричности отличается от традиционных языков тем, что все его команды состоят из одного символа. FALSE использует латинский алфавит, цифры, знаки пунктуации и некоторые особые обозначения, а также два специальных символа — ß и ø. Обо всем этом, я расскажу подробнее ниже.

Кроме того, FALSE — стековый язык программирования, причем крайне стековый, так как единственное, что нам будет доступно кроме простого ввода/вывода, это стек данных. В стек данных могут быть размещены целые числа, символы (как подвид целого числа) и также (сюрприз!) лямбда-функции и переменные, коих, как и функций может быть ровно 26 — по числу доступных букв латинского алфавита.

FALSE, помимо всего прочего, позволяет размещение комментариев, которые заключаются в фигурные скобки, и просто игнорируются интерпретатором.

Итак, самое простое, что мы можем уже сейчас сделать на FALSE — это положить число в стек, и для этого достаточно указать само число или последовательность чисел, которые один за другим будут размещены в стек.

Также, FALSE предлагает разумный набор операций, как математических, так и инструкций, манипулирующих стеком.

Давайте рассмотрим базовые инструкции FALSE, при этом, мы предполагаем, что необходимые данные уже размещены в стеке:

+  сложение
  вычитание (из вершины стека вычитается число под вершиной)
*  умножение
/   деление (вершина стека делится на число под вершиной)
_  изменение знака (унарный минус, ставиться после числа, либо применяется как самостоятельная операция)
$  дублировать элемент на вершине стека
% удалить вершину стека
\    поменять два верхних элемента стека местами
@ круговая перестановка трех верхних элементов стека
ø   копировать n-ый элемент от вершины стека на вершину стека

Также, нам доступны операции сравнения и логические операции (не путать с битовыми!), но тут есть один подвох, а именно, в трактовке интерпретатором числовых результатов данных операций: 0 обозначает false, а -1 обозначает true.

Данные операции выглядят так:

=   равенство
>   больше
&  логическое «И»
|    логическое «ИЛИ»
~   логическое «НЕ»

Как видите, отдельных операций для «не равно» и «меньше» не предусмотрено, однако, можно легко выкрутиться с помощью отрицания, выразив данные операции, как =~ и >~.

Набор более, чем скромный, но прежде чем идти дальше, я должен рассказать о том, что FALSE в качестве данных понимает только две сущности — целые числа и символы.

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

Строкового типа данных в FALSE не существует, но ничто не мешает использовать строки в программах на нем: двойные кавычки, в которые заключена некоторая строка, FALSE трактует как команду вывода в стандартный поток вывода всего, что содержится в кавычках.

В FALSE существуют глобальные переменные, имена которых могут состоять только из одной буквы. Присвоение значения переменной достаточно простое: указываете значение, затем имя переменной, после которой идет символ двоеточия. Данная операция связывает однобуквенную переменную с ее значением, а чтобы его извлечь (т.е. поместить в стек), необходимо указать имя переменной и после него указать символ точки с запятой. Простое упоминание переменной по ее имени кладет ее имя в стек данных без извлечения какого-либо значения.

А сейчас один замечательный момент: переменные в FALSE могут содержать не только числовые или символьные значения (не радуйтесь — строку сохранить в переменную у вас не получится), но и лямбда-функции!

Лямбда-функция в FALSE — это то, что окружено квадратными скобками. Простое заключение фрагмента кода на FALSE  в такие скобки помещает в стек то выражение, которое в них содержится и соответственно, при необходимости может быть оттуда извлечено. Таким образом, из-за стековой природы языка и возможности размещения исполняемого кода прямо в стеке, можно напрямую передавать одни функции и в другие функции, а также возникает мысль о том, что лямбда-выражениям FALSE не нужны аргументы, т.к. все необходимое уже лежит в стеке.

Теперь, понимая то, что FALSE без разницы, что перед ним переменная или функция, можно сделать вывод (и правильный!) о том, что для назначения функции имени необходимо просто поместить ее в переменную, воспользовавшись синтаксисом переменных: указываем выражение в квадратных скобках, которое содержит функцию, далее указываем имя и наконец ставим символ двоеточия.

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

А что насчет управляющих конструкций?

В FALSE есть две управляющие конструкции if и while.

Конструкция if выглядит достаточно просто сначала идет само условие (хотя и наличие условие необязательно — необходимое значение истинности может уже присутствовать на стеке), дальше лямбда-функция, которая выполняется в случае, если условие истинно, и далее идет знак вопроса, который и обозначает if.

Возможно, такое объяснение несколько путает, поэтому взглянем на следующий пример условной конструкции if в FALSE:

a;1=[3b:]?​

Расшифровать ее можно так: если a == 1, то b = 3.

Конструкции else не предусмотрено, однако, ее можно имитировать, скопировав со стека значения истинности для ветки if, в которой условие выполняется, а затем с помощью отрицания, повторив if, но уже указав ветку для которой условие ложно.

Вот смотрите пример данной идиомы:

a;1=$["true"]?~["false"]?​

Расшифровывается так: выполняем условие a == 1, затем дублируем результат выполнения условия, и если результат условия — true, выполняем печатаем «true», затем выполняем отрицание результата условия. Если отрицаемое условие истинно, то печатаем «false».

Поначалу, может быть сложно, но стоит попробовать — и становиться легче.

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

Вот пример типичного цикла while в FALSE:

[a;1=][2f;!]#

Расшифровать можно так: пока a == 1, выполняем применение функции f к 2.

А теперь можно переходить к тому, без чего не обходиться ни одна программа — к вводу/выводу.

Ввод/вывод в FALSE буферизован, поэтому перед началом операций, необходимо (в настоящий момент — не обязательно), сбросить потоки ввода/вывода. Это делается командой «ß», которая выполняет принудительный сброс всех потоков.

Ввод осуществляется с помощью команды ^, которая читает один символ из стандартного потока ввода. Вывод осуществляется с помощью двух различных команд — «.» и «,». Первая команда выведет целое число, а вторая — символ, иных же команд ввода/вывода не предусмотрено.

Кстати, забыл упомянуть, есть еще одна команда вывода — двойные кавычки. Данная команда выводит строку, которая не может быть положена в стек, но строка в двойных кавычках может быть частью лямбда-выражения.

На этом обзор языка окончен, прилагаю ниже краткую памятку по языку:

элементарные функции:
"+"	"-"	"*"	"/"	"_" (унарный минус)
"="	">" "~=" (0 = false, -1 = true)

типы данных:
- целые числа (например, 1, 10 и т.д)
- символы (начинаются с апострофа, например 'A)

переменные:
- от a до z (всего 26)
- ":" присваивание ";" извлечение

лямбда:
[] все что в таких скобках - это лямбда-функция (это выражение помещает функцию в стек)
! применение функции 

стековые функции:
(вершина - это один элемент)
	"$"	скопировать вершину стека (dup)		
	"%"	удалить вершину стека (drop)
	"\"	поменять два верхних элемента (swap)
	"@"	(x,x1,x2-x1,x2,x)	повернуть три верхних элемента стека (rot)
	"ø"	(n-x) скопировать n-ый элемент на вершину стека (pick)

управление ходом программы:
условие[функция, которая выполняется, если оно истинно]?     if
[условие][функция, которая выполняется, если оно истинно]#   while

ввод/вывод:
  все потоки буферизованы
	"ß" сбросить потоки ввода/вывода
	"." печать целого
	"," печать символа
	"^" ввести символ с потока ввода

комментарии
{текст комментария}

Теперь перейдем к реализации на D.

В первую очередь, откажемся от специальных символов ß и ø, заменив их на отсутствующие в FALSE круглые скобки — ( и ), которые и будут обозначать эти две операции. Также, мы реализуем простой интерпретатор с некоторыми расширенными функциями, сделав его обычным консольным приложением.

Для начала создадим новый проект dub и назовем его modernFalse:

dub init modernFalse

Теперь, нам потребуется класс для описания стека, который возьмем из одной из статей блога, только несколько доработаем его под наши нужды: вырежем ряд методов, которые не нужны, и добавим три новых метода swap, rot и pick — так как данные операции очень удобны и соответствуют командам FALSE «\», «@»,»ø».

Поместим класс Stack(T) в файл utils/stack.d:

module utils.stack;

class Stack(T)
{
	private
	{
		T[] elements;
	}
	
	public
	{
		// добавление элемента в конец
		void push(T element)
		{
			elements ~= element;
		}
		
		// удалить один элемент
		auto pop()
		{
			T topN = elements[$-1];
			--elements.length;
			
			return topN;
		}
		
		// количество элементов
		size_t length() const @property
		{
			return elements.length;
		}
		
		// вершина стека
		T top() const @property
		{
			return elements[$-1];
		}
		
		// содержимое стека
		T[] content() @property
		{
			return elements;
		}
		
		// стек пустой ?
		bool empty() const @property
		{
			if (elements.length == 0)
			{
				return true;
			}
			else
			{
				return false;
			}
		}

		// поменять местами два верхних элемента
		void swap()
		{
			T top = pop();
			T bottom = pop();

			push(top);
			push(bottom);
		}

		// переставить три верхних элемента по кругу
		void rot()
		{
			T first = pop;
			T second = pop;
			T third = pop;
			
			push(second);
			push(first);
			push(third);
		}

		// copy n-th element to the top
		void pick()
		{
			import std.conv : to;
			int n = pop.to!int;
			push(elements[$-n-1]);
		}
	}
}

Также, возьмем простой класс для раскрашивания терминала ColoredTerminal, который также рассматривался в нашей статье «Цветной вывод текста в терминале Linux«, и поместим его в файл utils/colorize.d.

Изменения в данный класс мы не вносим, и используем его, как есть.

Вот теперь начнем самое интересное: реализация интерпретатора FALSE.

Реализацию интерпретатора я позаимствовал у одного разработчика на Scala (он упомянут как автор, в информации о пакете modernFalse), соотвественно я сделал перевод на D и уточнил ряд моментов.

Начнем с того, что код на FALSE — это строка, которая манипулирует стеком. Команды самого языка односимвольные, следовательно, никаких разделителей между ними нет, хотя иногда и встречаются пробелы, которые разделяют данные помещаемые в стек. Именно, этим фактом мы и воспользуемся: будем двигаться по строке кода, попутно извлекая некоторые наиболее важные для нас элементы языка, такие как целые числа (метод getInteger, который накапливает в строку число), строки (метод getString, который накапливает элементы строки), комментарии (метод getComment, накапливающий содержимое комментариев) и лямбда-выражения (метод getFunction, анализирует и накапливает лямбда-выражения, используя для этого собственный стек).

Основной разбор самого языка осуществляется в методе execute, который просто сопоставляет односимвольные команды с реальными действиями над стеком или потоками ввода/вывода. Отличительной особенностью практически всех сопоставлений является непрерывное приведение типов от обычных строк к целым и затем опять в строки, и множество других преобразований. Такое обилие конверсии типов связано с тем, что в качестве универсального типа для хранения данных в стеке, выбран тип string, поскольку именно строка позволяет сохранить единообразность хранимой в стеке информации (помним о том, что у нас в одном и том же стеке могут лежать и данные и программные конструкции, что можно использовать в своих целях).

Разбор кода на FALSE задействует метод addToProgram, который позволяет адресно вырезать интересующий фрагмент кода из основной строки и добавляющий к нему другой фрагмент, что неплохо используется для выделения условий и лямбда-выражений. Метод execute в некоторых местах активно использует метод addToProgram.

В FALSE исконной реализации (от автора языка) была одна интересная функция, которая обозначалась косым апострофом («`»), которая обозначала прямую вставку машинного кода. В той реализации на Scala, которую я использовал для своих целей, эта команда была подменена — данная команда переназначена на другую операцию, а именно, на поворот n-ого элемента стека (не совсем понял, как это). Но, я уже сам давно присмотрелся к замене данной команды и решил добавить ряд недостающих операций, а также обеспечить почву для возможного добавления других команд, но об этом я расскажу чуть позже, сейчас я покажу, как мне удалось сделать замену и на что.

Итак, команда «косой апостроф» теперь вызывает заранее предопределенную операцию над стеком FALSE, и которая имеет некий числовой «адрес». Этот «адрес», представляет собой строку с номером объекта-функции, и используется как ключ в словаре предопределенных функций. «Адрес» кладется на стек данных сразу после данных, над которыми будет производиться операция, и я уже определил ряд нужных адресов (об этом чуть позже).

Чтобы это работало словарь помещается во внутреннюю приватную переменную класса, в которую из конструктора класса загружается заранее подготовленный словарь с предопределенными функциями, и именно его ВЫ можете изменять!

Поместим наш класс разбора кода FalseInterpreter в core/interpreter.d:

module utils.stack;

class Stack(T)
{
	private
	{
		T[] elements;
	}
	
	public
	{
		// добавление элемента в конец
		void push(T element)
		{
			elements ~= element;
		}
		
		// удалить один элемент
		auto pop()
		{
			T topN = elements[$-1];
			--elements.length;
			
			return topN;
		}
		
		// количество элементов
		size_t length() const @property
		{
			return elements.length;
		}
		
		// вершина стека
		T top() const @property
		{
			return elements[$-1];
		}
		
		// содержимое стека
		T[] content() @property
		{
			return elements;
		}
		
		// стек пустой ?
		bool empty() const @property
		{
			if (elements.length == 0)
			{
				return true;
			}
			else
			{
				return false;
			}
		}

		// поменять местами два верхних элемента
		void swap()
		{
			T top = pop();
			T bottom = pop();

			push(top);
			push(bottom);
		}

		// переставить три верхних элемента по кругу
		void rot()
		{
			T first = pop;
			T second = pop;
			T third = pop;
			
			push(second);
			push(first);
			push(third);
		}

		// copy n-th element to the top
		void pick()
		{
			import std.conv : to;
			int n = pop.to!int;
			push(elements[$-n-1]);
		}
	}
}


module core.interpreter;

private
{
	import std.ascii;
	import std.conv;
	import std.stdio;
	import std.string;

	import core.extended;

	import utils.colorize;
	import utils.stack;
}

// FALSE programming language interpreter
class FalseInterpreter
{
	private
	{
		// current FALSE program
		string falseProgram;
		// FALSE stack
		Stack!string stack;
		// FALSE variables and function storage
		string[string] variables;
		// features of modernFalse
		FunctionStorage storage;

	// add program fragment
	auto addToProgram(int position, string program, string func)
	{
		string programBegin = program[0..position+1];
		string programEnd = "";

		if ((position + 1) != program.length)
		{
			programEnd = program[position+1..$];
		}

		return programBegin ~ func ~ programEnd;
	}

	// get integer from fragment of FALSE program
	auto getInteger(int startPosition)
	{
		string result;
		
		for (int i = startPosition; i < falseProgram.length; i++)
		{
			if (falseProgram[i].isDigit)
			{
				result = result ~ falseProgram[i].to!string;
			}
			else
			{
				break;
			}
		}

		return result;
	}

	// get string from fragment of FALSE program
	auto getString(int startPosition)
	{
		string result;
		
		for (int i = startPosition; i < falseProgram.length; i++)
		{
			if (falseProgram[i] != '\"')
			{
				result = result ~ falseProgram[i].to!string;
			}
			else
			{
				break;
			}
		}

		return result;
	}

	// get comment from fragment of FALSE program
	auto getComment(int startPosition)
	{
		int commentLength = 0;
		
		for (int i = startPosition; i < falseProgram.length; i++)
		{
			if (falseProgram[i] != '}')
			{
				commentLength = commentLength + 1;
			}
			else
			{
				break;
			}
		}

		return commentLength;
	}

	// get function from fragment of FALSE program
	auto getFunction(int startPosition)
	{
		string result;
		Stack!char bracketStack = new Stack!char;
		
		bracketStack.push('[');

		for (int i = startPosition; i < falseProgram.length; i++)
		{
			if (falseProgram[i]  == '[')
			{
				bracketStack.push('[');
				result = result ~ falseProgram[i].to!string;
			}
			else
			{
				if ((falseProgram[i] == ']') && (bracketStack.length == 1) && (bracketStack.top == '['))
				{
					break;
				}
				else
				{
					if (falseProgram[i] == ']')
					{
						bracketStack.pop;
						result = result ~ falseProgram[i].to!string;
					}
					else
					{
						result = result ~ falseProgram[i].to!string;
					}
				}
			}
		}

		return result;
		
		}
	}

	this()
	{
		stack = new Stack!string;
		storage = loadExtendedFunctions;
	}

	// execute FALSE program
	auto execute(string mainProgram)
	{
		string fragmentOfProgram = mainProgram;
		falseProgram = mainProgram;
		
		int i = 0;
		
		while (i < falseProgram.length) { char current = falseProgram[i]; if (current.isDigit) { string number = getInteger(i); stack.push(number); i = i + number.length.to!int - 1; } else { if (current.isAlpha) { stack.push("" ~ current.to!string); } else { switch (current) { case '{': i = i + getComment(i+1) + 1; break; case '_': stack.push((-1 * stack.pop.to!int).to!string); break; case '+': stack.push((stack.pop.to!int + stack.pop.to!int).to!string); break; case '-': stack.swap; stack.push((stack.pop.to!int - stack.pop.to!int).to!string); break; case '*': stack.push((stack.pop.to!int * stack.pop.to!int).to!string); break; case '/': stack.swap; stack.push((stack.pop.to!int / stack.pop.to!int).to!string); break; case '=': if (stack.pop == stack.pop) { stack.push("-1"); } else { stack.push("0"); } break; case '~': if (stack.pop == "0") { stack.push("-1"); } else { stack.push("0"); } break; case '>':
							int a = stack.pop.to!int;
							int b = stack.pop.to!int;
							if (b > a)
							{
								stack.push("-1");
							}
							else
							{
								stack.push("0");
							}
							break;
						case '&':
							int a = stack.pop.to!int;
							int b = stack.pop.to!int;
							if ((a == 0) || (b == 0))
							{
								stack.push("0");
							}
							else
							{
								stack.push("-1");
							}
							break;
						case '|':
							int a = stack.pop.to!int;
							int b = stack.pop.to!int;
							if ((a == -1) || (b == -1))
							{
								stack.push("-1");
							}
							else
							{
								stack.push("0");
							}
							break;
						case '$':
							stack.push(stack.top);
							break;
						case '%':
							stack.pop;
							break;
						case '\\':
							stack.swap;
							break;
						case '@':
							stack.rot;
							break;
						case 'ø', '(':
							stack.pick;
							break;
						case '\'':
							string character = falseProgram[i+1].to!int.to!string;
							stack.push(character);
							i = i + 1;
							break;
						case ':':
							string key = stack.pop;
							string value = stack.pop;
							variables[key] = value;
							break;
						case ';':
							string key = stack.pop;
							stack.push(variables[key]);
							break;
						case '.':
							try
							{
								string result = stack.pop;
								std.stdio.write(result);
							}
							catch (Exception e)
							{
								ColoredTerminal.error("Stack empty").writeln;
							}
							break;
						case ',':
							string value = stack.pop;
							try
							{
								std.stdio.write(value.parse!int.to!char);
							}
							catch (Exception e)
							{
								std.stdio.write(value);
							}
							break;
						case '\"':
							string s = getString(i+1);
							std.stdio.write(s);
							i = i + s.length.to!int + 1;
							break;
						case '^':
							try
							{
								char expression;
								readf("%c", expression);
								string character = expression.to!int.to!string;
								stack.push(character);
							}
							catch (Exception e)
							{
								stack.push("-1");
							}
							break;
						case 'ß', ')':
							stdin.flush;
							stdout.flush;
							// stack.pop
							break;
						case '[':
							string func = getFunction(i+1);
							stack.push(func);
							i = i + func.length.to!int + 1;
							break;
						case '!':
							string func = stack.pop;
							fragmentOfProgram = addToProgram(i, fragmentOfProgram, func);
							falseProgram = fragmentOfProgram;
							break;
						case '?':
							string func = stack.pop;
							string cond = stack.pop;
							if (cond == "-1")
							{
								fragmentOfProgram = addToProgram(i, fragmentOfProgram, func);
								falseProgram = fragmentOfProgram;
							}
							break;
						case '#':
							string func = stack.pop;
							string cond = stack.pop;
							string loop = cond ~ "[" ~ func ~ "[" ~ cond ~ "][" ~ func ~ "]#]?";
							fragmentOfProgram = addToProgram(i, fragmentOfProgram, loop);
							falseProgram = fragmentOfProgram;
							break;
						case '`':
							string feature = stack.pop;
							if (feature in storage)
							{
								ModernFalseFunction mff = storage[feature];
								mff(stack);
							}
							else
							{
								ColoredTerminal.error("Feature with this number is not defined. You can define her yourself in file core/extended.d").writeln;
							}
							break;
						default:
							break;
					}
				}
			}
			i = i + 1;
		}
		writeln;
	}
}

Теперь, как и обещалось, я расскажу про то, как я внес улучшения в FALSE.

Сначала определяем абстрактный класс ModernFalseFunction с одним единственным методом — «магическим». Этот метод называется opCall, и он определяется для таких объектов, которые можно вызывать как функции, и именно это нам и надо. В сигнатуре opCall стоит стек строк, который передается по ссылке, что обеспечивает возможность его изменения:

// feature class
abstract class ModernFalseFunction
{
	void opCall(ref Stack!string stack);
}

Определим хранилище всех новых функций, как словарь, и для удобства создадим псевдоним:

// alias for type of storage for new features
alias FunctionStorage = ModernFalseFunction[string];

А чтобы определить свою функцию-расширение языка FALSE достаточно унаследоваться от класса ModernFalseFunction и переопределить метод opCall, который делает необходимые преобразования стека. Я, к примеру, таким образом добавил поддержку битовых операций:

// bitwise and
class AndFunction : ModernFalseFunction
{
	override void opCall(ref Stack!string stack)
	{
		stack.swap;
		stack.push((stack.pop.to!int & stack.pop.to!int).to!string);
	}
}

// bitwise or
class OrFunction : ModernFalseFunction
{
	override void opCall(ref Stack!string stack)
	{
		stack.swap;
		stack.push((stack.pop.to!int | stack.pop.to!int).to!string);
	}
}

// bitwise xor
class XorFunction : ModernFalseFunction
{
	override void opCall(ref Stack!string stack)
	{
		stack.swap;
		stack.push((stack.pop.to!int ^ stack.pop.to!int).to!string);
	}
}

// bitwise not
class NotFunction : ModernFalseFunction
{
	override void opCall(ref Stack!string stack)
	{
		stack.swap;
		stack.push((~(stack.pop.to!int)).to!string);
	}
}

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

Помещаем все дополнительные функции в файл core.extended:

module core.extended;

private
{
	import std.conv : to;

	import utils.stack;
}

// feature class
abstract class ModernFalseFunction
{
	void opCall(ref Stack!string stack);
}

// alias for type of storage for new features
alias FunctionStorage = ModernFalseFunction[string];

private
{
	// push number of stack items to top
	class DepthFunction : ModernFalseFunction
	{
		override void opCall(ref Stack!string stack)
		{
			stack.push(stack.length.to!string);
		}
	}

	// bitwise and
	class AndFunction : ModernFalseFunction
	{
		override void opCall(ref Stack!string stack)
		{
			stack.swap;
			stack.push((stack.pop.to!int & stack.pop.to!int).to!string);
		}
	}

	// bitwise or
	class OrFunction : ModernFalseFunction
	{
		override void opCall(ref Stack!string stack)
		{
			stack.swap;
			stack.push((stack.pop.to!int | stack.pop.to!int).to!string);
		}
	}

	// bitwise xor
	class XorFunction : ModernFalseFunction
	{
		override void opCall(ref Stack!string stack)
		{
			stack.swap;
			stack.push((stack.pop.to!int ^ stack.pop.to!int).to!string);
		}
	}

	// bitwise not
	class NotFunction : ModernFalseFunction
	{
		override void opCall(ref Stack!string stack)
		{
			stack.swap;
			stack.push((~(stack.pop.to!int)).to!string);
		}
	}
}

// load all features
auto loadExtendedFunctions()
{
	// place for new features
	FunctionStorage features = [
		"0" : (new DepthFunction),
		"1" : (new AndFunction),
		"2" : (new OrFunction),
		"3" : (new XorFunction),
		"4" : (new NotFunction), 
	];

	return features;
}

А теперь финальный штрих — создаем процедуру main (помещаем ее в файл app.d), которая будет реализовывать простейший REPL, наподобие того, что был в одной из предыдущих статей:

import std.conv : to;
import std.stdio;
import std.string : chomp;

import core.interpreter;
import utils.colorize;

void main()
{
	FalseInterpreter falseInterpreter = new FalseInterpreter;
	
	while (!stdin.eof)
	{
		ColoredTerminal.colorize2("False> ", "", "lightGreen").write;
		char[] falseExpression;
		stdin.readln(falseExpression);
		
		string falseCommand = to!string(chomp(falseExpression));
		falseInterpreter.execute(falseCommand);
	}
}

Компилируем, запускаем и прогоняем ряд примеров (взяты из тест-кейсов реализации на Scala, а также добавил немного своих примеров):

И напоследок несколько программ на FALSE (а более интересные примеры вы можете найти на сайте языка или создать сами!):

{16 чисел Фиббоначи} 0i:1a:1b:[i;16=~][a;$.", "$b;$a:+b:i;1+i:]#"..."
{Простые числа от 100 до 1} [\$@$@\/*-0=]d:  {Test if p divides q}[$2/[\$@$@\d;!~][1-]#1=\%]p:  {Is p prime?}[[$1=~][$p;![$." "]?1-]#]f:  {for all i from n to 2 do { if i is prime then print i} }99f;!
{Квайн}["'[,34,$!34,'],!"]'[,34,$!34,'],!
{Бесконечное считывание потока ввода} )[^$1_=~][,]#
{Вывод транслитом имени "Полина"} [$,]a:80a;!31+a;![3-a;!]b:b;!b;!5+a;!13-,
{Вывод всех букв и некоторых иных знаков} 123a:[a;65=~][a;1-$a:,]#

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

Добавить комментарий