Генератор на perl конечного автомата на C.

Идея написать статью возникла, когда наткнулся на easyelectronics.ru/avr-uchebnyj-kurs-konechnyj-avtomat.html

В данной статье рассматривается принцип работы сгенерированного кода, сам генератор написан на perl и в нём особо интересного ничего нет, берите, пользуйтесь, модифицируйте под свои нужды :)

По сути, предыстория написания программы-генератора: некоторое время назад я делал несколько мелких программ для нового проекта, все они различались реакциями на события. День тупого полумеханического труда по написанию почти одного и того же и к вечеру у меня сдали нервы. Попробовал поискать какой-нибудь готовый генератор кода для конечного автомата (КА) или хотя бы таблиц из некого формализованного описания, ведь по виду кажется просто — есть граф, генерируй-нехочу. Главными критериями были простота и чтоб в результате был код на чистом C, без всякиих плюсплюсов — мне же для микроконтроллера! Но тут меня ждал облом. Либо не просто, либо не C, либо вообще монструозно. В общем, ничего подходящего, не нашёл. К обеду следующего дня у меня был простенький наколенный генератор на perl, как в мультике — лучше день потерять зато потом за пять минут долететь :). Потом уже, когда его стали использовать коллеги, дописал некоторые вещи по их замечаниям. В конце концов получилось нечто, что уже не раз пригождалось узкому кругу людей, и что я и хочу представить на суд общественности, может кому-то ещё поможет.

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

Типичный представитель КА:

enum commands {
	cmdA, cmdB, cmdC
};

enum states {
	stateA, stateB, stateBB, stateC
};

int state = stateA;

...

switch (cmd) {
	case cmdA:
		switch (state) {
			case stateA:
				state = stateB;
				break;
			case stateB:
				state = stateBB;
				break;
			case stateBB:
				state = stateB;
				break;
			default:
				state = stateA;
				break;
		}
		break;
	case cmdB:
		switch (state) {
			case stateBB:
				state = stateC;
				break;
			default:
				state = stateA;
				break;
		}
		break;
	case cmdC:
		state = stateA;
		break;
}

Здесь мы попадаем в состояние stateC получив подряд две команды cmdA и одну cmdB, а команда cmdC сбрасывает в начальное состояние stateA.

Реализуют также транспонированный вариант вида:

...

switch (state) {
	case stateA:
		switch (cmd) {
			case cmdA:
			case cmdB:
			case cmdC:

...

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

Несколько улучшив код, можно получить следующий вариант:

...

int fsm_table[3][4] = {
/*		 stateA   stateB   stateBB   stateC	*/
/* cmdA */	{stateB,  stateBB, stateB,   stateA, },
/* cmdB */	{stateA,  stateA,  stateC,   stateA, },
/* cmdC */	{stateA,  stateA,  stateA,   stateA, },

...

state = fsm_table[cmd][state];

};

Размер текста значительно уменьшается и примерно в подобном виде я строю КА в своих программах.
Но остаётся основная проблема: написание таблицы, что по трудозатратам никак не лучше чем switch-case, а внимательности нужно даже больше.

Итак, поведу описание на основе графа работы устройства, представляющего собой ноду в сети CANopen.
Картинка из документации:


Состояния изменяются по команде извне мастер-нодой. Мастер-нода может запростить состояние ноды.

Стадия Initialisation состоит из трёх промежуточных стадий, описанных следующим графом:


В этой стадии до состояния Pre-Operational изменяются самой нодой по завершению необходимых действий. Но, собственно, с точки зрения КА нас это почти не интересует :)

Опишем список состояний во входном файле генератора:

/* Подсостояния инициализации */
state stateHwReset		0x00
state stateAppReset		0x00
state stateCommReset		0x00
/* Состояния */
state stateInitialisation	0x00
state stateStopped		0x04
state stateOperational		0x05
state statePreOp		0x7F

Из этого будет генерироваться следующий код:

typedef enum co_e_states {
/* Подсостояния инициализации */
	stateHwReset = 0x00,
	stateAppReset = 0x00,
	stateCommReset = 0x00,
/* Состояния */
	stateInitialisation = 0x00,
	stateStopped = 0x04,
	stateOperational = 0x05,
	statePreOp = 0x7F,
} co_states_t;

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

typedef enum co_e_states_num {
/* _stateInvalid - технологическое состояние, нуждается в дополнительной обработке */
	_stateInvalid = -1,
	_stateHwReset = 0,
	_stateAppReset = 1,
	_stateCommReset = 2,
	_stateInitialisation = 3,
	_stateStopped = 4,
	_stateOperational = 5,
	_statePreOp = 6,
} co_states_num_t;

co_states_t co_states[7] = {
	stateHwReset,
	stateAppReset,
	stateCommReset,
	stateInitialisation,
	stateStopped,
	stateOperational,
	statePreOp,
};

Теперь опишем список команд:

/* Внутренняя команда */
command cmdInitialising		0x00
/* Внешние команды */
command cmdStartOp		0x01
command cmdStop			0x02
command cmdPreOp		0x80
command cmdReset		0x81
command cmdResetComm		0x82

Из этого будет генерироваться следующий код:

typedef enum co_e_commands {
	cmdInitialising = 0x00,
	cmdStartOp = 0x01,
	cmdStop = 0x02,
	cmdPreOp = 0x80,
	cmdReset = 0x81,
	cmdResetComm = 0x82,
} co_commands_t;

И снова видим, что номера не являются порядковыми, делаем тоже самое что с состояниями:

typedef enum co_e_commands_num {
	_cmdInitialising = 0,
	_cmdStartOp = 1,
	_cmdStop = 2,
	_cmdPreOp = 3,
	_cmdReset = 4,
	_cmdResetComm = 5,
} co_commands_num_t;

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

co_commands_num_t c;
switch(cmd) {
	case cmdInitialising:
		c = _cmdInitialising;
		break;
	case cmdStartOp:
		c = _cmdStartOp;
		break;
	case cmdStop:
		c = _cmdStop;
		break;
	case cmdPreOp:
		c = _cmdPreOp;
		break;
	case cmdReset:
		c = _cmdReset;
		break;
	case cmdResetComm:
		c = _cmdResetComm;
		break;
}

Опишем список переходов:

/*      команда(текущиее состояние)	результирующее состояние */
transit cmdInitialising(stateHwReset)	stateInitialisation
transit cmdPreOp(stateCommReset)	statePreOp
transit cmdStartOp(statePreOp)		stateOperational
transit cmdPreOp(stateOperational)	statePreOp
transit cmdStop(statePreOp)		stateStopped
transit cmdStartOp(stateStopped)	stateOperational
transit cmdPreOp(stateStopped)		statePreOp
transit cmdStop(stateOperational)	stateStopped
transit cmdReset(stateOperational)	stateAppReset
transit cmdReset(stateStopped)		stateAppReset
transit cmdReset(statePreOp)		stateAppReset
transit cmdResetComm(stateOperational)	stateCommReset
transit cmdResetComm(stateStopped)	stateCommReset
transit cmdResetComm(statePreOp)	stateCommReset
transit cmdReset(stateInitialisation)	stateAppReset
transit cmdResetComm(stateAppReset)	stateCommReset

Забегая чуть-чуть вперёд, скажу что я распложил список так, чтоб получить номера переходов такие же, как на картинках графов перехода ноды, хотя для работы программы это не имеет значения.

Теперь мы готовы к генерации таблицы, из которой будем брать новое состояние. Но вот тут появляется проблема, заключающаяся в том, что для выполнения необходимых действий нам нужно знать не только новое состояние, но и из какого мы в него попали. Для этого будем использовать список переходов, который будет однозначно идентифицировать пару «откуда-куда»:

typedef enum co_e_transits {
	co_transit_Invalid = 0,
	transit_cmdInitialising_stateHwReset = 1,
	transit_cmdStartOp_stateStopped = 6,
	transit_cmdStartOp_statePreOp = 3,
	transit_cmdStop_stateOperational = 8,
	transit_cmdStop_statePreOp = 5,
	transit_cmdPreOp_stateCommReset = 2,
	transit_cmdPreOp_stateStopped = 7,
	transit_cmdPreOp_stateOperational = 4,
	transit_cmdReset_stateInitialisation = 15,
	transit_cmdReset_stateStopped = 10,
	transit_cmdReset_stateOperational = 9,
	transit_cmdReset_statePreOp = 11,
	transit_cmdResetComm_stateAppReset = 16,
	transit_cmdResetComm_stateStopped = 13,
	transit_cmdResetComm_stateOperational = 12,
	transit_cmdResetComm_statePreOp = 14,
} co_transits_t;

Здесь co_transit_Invalid — это, скорее, переход по умолчанию, но уж как назвал :)

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

co_states_num_t co_transitstate[17] = {
	_stateInvalid,
	_stateInitialisation,
	_statePreOp,
	_stateOperational,
	_statePreOp,
	_stateStopped,
	_stateOperational,
	_statePreOp,
	_stateStopped,
	_stateAppReset,
	_stateAppReset,
	_stateAppReset,
	_stateCommReset,
	_stateCommReset,
	_stateCommReset,
	_stateAppReset,
	_stateCommReset,
};

И, собственно, сама матрица переходов:

co_transits_t co_transitions[6][7] = {
	{
		transit_cmdInitialising_stateHwReset,
		co_transit_Invalid,
		co_transit_Invalid,
		co_transit_Invalid,
		co_transit_Invalid,
		co_transit_Invalid,
		co_transit_Invalid,
	},
	{
		co_transit_Invalid,
		co_transit_Invalid,
		co_transit_Invalid,
		co_transit_Invalid,
		transit_cmdStartOp_stateStopped,
		co_transit_Invalid,
		transit_cmdStartOp_statePreOp,
	},
	{
		co_transit_Invalid,
		co_transit_Invalid,
		co_transit_Invalid,
		co_transit_Invalid,
		co_transit_Invalid,
		transit_cmdStop_stateOperational,
		transit_cmdStop_statePreOp,
	},
	{
		co_transit_Invalid,
		co_transit_Invalid,
		transit_cmdPreOp_stateCommReset,
		co_transit_Invalid,
		transit_cmdPreOp_stateStopped,
		transit_cmdPreOp_stateOperational,
		co_transit_Invalid,
	},
	{
		co_transit_Invalid,
		co_transit_Invalid,
		co_transit_Invalid,
		transit_cmdReset_stateInitialisation,
		transit_cmdReset_stateStopped,
		transit_cmdReset_stateOperational,
		transit_cmdReset_statePreOp,
	},
	{
		co_transit_Invalid,
		transit_cmdResetComm_stateAppReset,
		co_transit_Invalid,
		co_transit_Invalid,
		transit_cmdResetComm_stateStopped,
		transit_cmdResetComm_stateOperational,
		transit_cmdResetComm_statePreOp,
	},
};

Теперь логика работы всего этого хозяйства такова:
Допустим, нода находится в состоянии Pre-Operational.
Переменная co_state хранит _statePreOp = 6.
Внешнее значение состояния co_states[6] = statePreOp = 0x7F.
Приходит команда cmdStartOp, с помощью вышеуказанного switch получаем номер команды _cmdStartOp = 1.
Номер перехода из таблицы co_transitions[1][6] = transit_cmdStartOp_statePreOp = 3.
Новое состояние из таблицы co_transitstate[3] = _stateOperational = 5.
Внешнее значение состояния co_states[5] = stateOperational = 0x05.
Можем выполнять действия, связанные с переходом transit_cmdStartOp_statePreOp, например, храним указатели на функции-обработчики в массиве и берём по номеру.

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

co_transits_t co_transit(co_states_num_t state, co_commands_t cmd)
{
	co_commands_num_t c;
	switch(cmd) {
		case cmdInitialising:
			c = _cmdInitialising;
			break;
		case cmdStartOp:
			c = _cmdStartOp;
			break;
		case cmdStop:
			c = _cmdStop;
			break;
		case cmdPreOp:
			c = _cmdPreOp;
			break;
		case cmdReset:
			c = _cmdReset;
			break;
		case cmdResetComm:
			c = _cmdResetComm;
			break;
		default:
			return co_transit_Invalid;
			break;
	}
	return co_transitions[c][state];
}

А также напишем функцию-интерфейс для получения перехода и одноременного изменения состояния:

co_transits_t co_transitState(co_commands_t cmd)
{
	co_transits_t t = co_transit(co_state, cmd);
	if(t != co_transit_Invalid) {
		co_state = co_transitstate[t];
	}
	return t;
}

И функцию, возвращающую код состояния:

co_states_num_t co_getState()
{
	return co_states[co_state];
}

И вариант использования в коде:

#include <stdio.h>
#include "sample.h"

char *name(int state)
{
	char *n;
	switch (state) {
		case 0x00:
			n = "stateInitialisation";
			break;
		case 0x04:
			n = "stateStopped";
			break;
		case 0x05:
			n = "stateOperational";
			break;
		case 0x7F:
			n = "statePreOp";
			break;
	}
	return n;
}

extern co_states_num_t co_state;

int queue[] = {cmdInitialising, cmdReset, cmdResetComm, cmdPreOp,
	cmdStartOp, cmdStop, cmdPreOp,
	cmdStop, cmdStartOp, cmdPreOp,
	cmdResetComm,
};

int main()
{
	int i, n, t;

	n = sizeof(queue)/sizeof(queue[0]);

	printf("Initial state: 0x%.2x (%d): %s\n", co_getState(), co_state, name(co_getState()));
	for (i = 0; i < n; i++) {
		t = co_transitState(queue[i]);
		printf("transit: %.2d, state: 0x%.2x (%d): %s\n",
			t, co_getState(), co_state, name(co_getState()));
	}

	return 0;
}

Данная программа выдаёт следующий результат при запуске:

transit: 01, state: 0x00 (3): stateInitialisation
transit: 15, state: 0x00 (1): stateInitialisation
transit: 16, state: 0x00 (2): stateInitialisation
transit: 02, state: 0x7f (6): statePreOp
transit: 03, state: 0x05 (5): stateOperational
transit: 08, state: 0x04 (4): stateStopped
transit: 07, state: 0x7f (6): statePreOp
transit: 05, state: 0x04 (4): stateStopped
transit: 06, state: 0x05 (5): stateOperational
transit: 04, state: 0x7f (6): statePreOp
transit: 14, state: 0x00 (2): stateInitialisation


Добавил:

Визуализация результата:
Есть такая программа Graphviz. Присуствует во многих дистрибутивах линукса, на сайте есть версия под винду. Теперь скрипт создаёт файл графа для неё:

digraph co_ {
	0[style=filled,label="stateHwReset\n0x00"];
	1[label="stateAppReset\n0x00"];
	2[label="stateCommReset\n0x00"];
	3[label="stateInitialisation\n0x00"];
	4[label="stateStopped\n0x04"];
	5[label="stateOperational\n0x05"];
	6[label="statePreOp\n0x7F"];
	0->3[label="(1)"];
	4->5[label="(6)"];
	6->5[label="(3)"];
	5->4[label="(8)"];
	6->4[label="(5)"];
	2->6[label="(2)"];
	4->6[label="(7)"];
	5->6[label="(4)"];
	3->1[label="(15)"];
	4->1[label="(10)"];
	5->1[label="(9)"];
	6->1[label="(11)"];
	1->2[label="(16)"];
	4->2[label="(13)"];
	5->2[label="(12)"];
	6->2[label="(14)"];
}

Запустив который командой
dotty sample.dot
можно увидеть граф:


И помните, всё-таки волшебства не будет, самостоятельно подумать и что-то написать всё равно придётся, данная программа лишь упрощает процесс.

— Проект генератора с примером можно скачать здесь:
bitbucket.org/demitel/dfsm/downloads
или командой git
git clone https://demitel@bitbucket.org/demitel/dfsm.git

Или во вложении к статье.

Замечания и коментарии конечно же приветствуются.
  • +4
  • 22 февраля 2015, 00:55
  • demitel
  • 1
Файлы в топике: dfsm.zip

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

RSS свернуть / развернуть
Вот тут ИС Дракон и пригодилась бы. и сразу в граф. представлении.
+1
  • avatar
  • x893
  • 22 февраля 2015, 12:14
тоже про него и подумал, а то уж как-то сложно все выглядит
0
А что в Драконе можно нарисовать мышкой граф состояний и он выдаст такую портянку на Си с case'ами/if'ами?
0
Конечно — только программа корявенькая, но в принципе не сложно.
Обсуждают её тут
forum.easyelectronics.ru/viewtopic.php?f=13&t=8992&start=650
0
Я ни в коем случае не хочу критиковать или умалять достоинства чужой разработки. Огромное количество людей программирует для микроконтроллеров не только на си или асме, но, например, на ардуино, паскале, даже на яве или релейной логике. Пусть будет кому-то Дракон и LabView и это будет здорово :) Это неким образом снижает уровень вхождения в программирование, что не может не радовать. Но лично моё мнение, что каждому инструменту своё применение, и конкретно этот скрипт на перле реализует возможность упростить работу для тех, кто по старинке лабает свои проги в тексте на си, потому что это брутально, это повышает самомнение и тешит чсв :)
Спасибо :)
0
:) да я и не возражаю
0
В эпоху говнокода и привлечения огромных масс народа еще со школы к ремеслу кодирования, с Си в эмбедде надо сотворить тоже, что и с asm в свое время — превратить его в промежуточную отрыжку IDE, листинги которой при желании можно посмотреть, но лучше не лазить туда ручонками. Погнать, так называемых «программистов», ссаными тряпками, что уже сделали в некоторых отраслях, например, в промавтоматике, применив специфические языки и инструментальные ср-ва, т.е. выжгли каленым железом всех «гуру» потенциально опасных языков(типа Си) и прочих сторонников «творчества» и «самодокументированного кода» :DDDD

Кодера в его IDE надо обложить со всех сторон фреймворками, настройщиками свойств объектов MCU(периферии и RTOS), разными визардами, редакторами графов состояний, UML и блок-схем. Т.е. программировать_он_должен_мышкой (фраза достойная лурки).

Половина говнокодеров, ранее питавшихся с web'а и корпоративно-бухгалтерского сектора, бросится программировать интернет_вещей. И вот тогда-то я и заброшу эту всю MCU-богадельню и займусь чем-нибудь НОВЫМ и более интересным и достойным для homo sapiens'а.

Но сейчас можно еще понаслаждаться чтением толстых даташитов и многочасовым ползаньем по портянкам хидеров библиотек. Я не думаю, что это умное занятие и оно достойно человека XXI века или как-то развивает его мозг — совсем наоборот, но… это тешит мое ЧСВ!
0
Т.е. программировать_он_должен_мышкой

Не получится. Могу привести пример с .NET. Программировать мышкой можно уже очень давно (Windows Workflow Foundation). Только это неудобно по сравнению с традиционным подходом. Диаграммы и всякие схемы – их тоже дофига. Например, модель в EE. Но работать с большой моделью тяжело (куча связей, которые на большой схеме становятся нечитаемыми). Мержить схему – обще ад, не смотря на то, что внутри она является XML файлом.
Идея – «программировать мышкой» существует давно, отдельные элементы такого декларативного подхода активно применяются, но в описании логики – такой подход не эффективен (особенно в больших проектах).
+1
Конечный автомат конечно прикольно в матричной реализации, первый раз увидел, как говорится кипятком писал.
Но протрахавшись кучу времени c заданием про светофор (в курсе на edx)понял что не так уж это и приятно при даже относительно небольшом количестве состояний.
зы Хотя если сделать визуализацию то может и будет толк-видел в одном применяемом продукте, но не использовал из за странной работы.
0
Ну, своим скриптом я и решал эту проблему — облегчить задачу по генерации матрицы.
То что я привёл — это работает в реальном проекте, описание КА — просто и логично, посмотрите входной файл.
Визуализация. Хмм… надо подумать, мысль есть.
0
Может быть много времени убил на задание из-за того, что не знал режима работы светофоров в США? Я перед выполнением задания специально посмотрел как работают светофоры. После этого таблицу состояний заполнил менее, чем за час. Откладка не понадобилась.
0
Просто в целях повышения образованности. А что бывают бесконечные автоматы? Дайте ссылку. Или не то с терминологией?
0
По русски почему-то их так называют, на английском это state machine, точнее finit state machine. Как мне кажется, английское название более точно отражает суть, да и русский дословный перевод, по-моему, звучит лучше — машина состояний (машина (автомат) с конечным числом состояний). Существует ещё и HSM — hierarchical state machine, как правильно называется по русски даже не знаю. Я бы назвал — иерархическая машина состояний, т. е. такая машина состояний, в которой существует ещё и иерархия состояний, т. е. когда одно состояние может содержать в себе другое/ие сосостояние/ия.
0
Ошибка — finite state machine
0
Вообще, если речь идет об автоматах (а не машинах состояний), то бывают DFA (determined finite automata) и NFA (non-determined finite automata). В первом случае переходы между состояниями однозначно определяется входными сигналами, во втором — по одному сигналу из одного состояния можно переходить в несколько или переходы между состояниями возможны без внешних воздействий и т.д.
0
На самом деле, ИМХО, действительно есть путаница в терминологии.

У вас в обоих случаях «конечный автомат» (finite automata), тобишь количество состояний автомата конечно. А вот условия перехода не обязательно должны быть однозначно определенны (determined)
0
Я бы вообще не пользовался термином «конечный автомат». Гораздо информативнее назвать автомат по причинам переключения в другое состояние. Например «флаговый автомат» или «автомат состояний». По мне логика становится более понятной. ИМХО.
0
Ну, это просто устоявшийся термин дискретной математики. Там важно деление на абстрактные/конечные, детерминированные и не детерминированные автоматы.
В программировании – обычно подразумевается конечный детерминированный автомат.
0
Все это конечно круто, заново изобрести велосипед когда уже есть самолет… Я по работе использую пакет StateFlow из Matlab Simulink. Генерирует код на C, с юнит тестами, под разные стандарты (MISRA), поддерживает временную логику (задержки). Код генерируется хороший, можно выбрать под аппаратную платформу, отлаживать логику в модели, зашить в контроллер и подавать на входы из Matlab нужные входные воздействия в реальном времени.
Это как бэ мейнстрим…
0
Как бы… Самолёт это круто, но в магазин я хожу пешком.
Глупо сравнивать поделку, которая просто может несколько упростить жизнь, и коммерческий продукт, который стоит денег. Я не собираюсь конкурировать с коммерческим продуктом. Собственно, я вообще ни с кем и ни с чем не собираюсь конкурировать, каждый может использовать скрипт на свой страх и риск, но бесплатно :) Мы же говорим о хоббистах, верно?
0
Так, чисто для примера, не похвастаться. У меня на работе есть 3D принтер фирмы стратасис, с поддержкой, который стоил конторе в далёком 2007 году офигенских денег, который может напечатать полый шар, и это будет реально шар а не то, что печатают современные дешёвые принтеры. Я и коллеги, пользуясь служебным положением, с удовольствием печатаем на нём время от времени себе какую-нибудь фигню :) Но мне совесть не позволит сказать хоть слово энтузиастам, собирающим принтеры из фанеры и ардуинок.
+1
нет, на самом деле респект и уважуха за труды, но по-моему просто имеет смысл изучить общепринятые инструменты для начала…
Если денег нет и Open Source есть Quantum Leaps Framework. Как раз для этого.
0
Трудов — на день. Если повспоминать и просуммировать. Достал, встряхнул от пыли, выложил. Не стоит переоценивать :)
«для начала…» изучают дискретную математику вообще и теорию конечных автоматов в частности.
StateFlow, Quantum Leaps Framework и ещё много чего — видел. Но и правда не чувствуете разницу с мини-мини скриптом? Ведь тем не менее, учитывая что зачастую нужно до десятка состояний и чуть больше команд, сгенерированный код работает. Довольно быстро работает. Да и что бы не работать, там ничего заумного нет. И «это слишком просто и тривиально» — как-то не аргумент :)
А вообще. По теме что-то есть сказать? Ну там, «а вот в 53 строчке фигня получилась», или будет только «у вас ус отклеился»? :)
Спасибо.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.