Научная статья на тему 'Разработка учебного языка программирования и интерпретатора'

Разработка учебного языка программирования и интерпретатора Текст научной статьи по специальности «Компьютерные и информационные науки»

CC BY
536
78
i Надоели баннеры? Вы всегда можете отключить рекламу.
i Надоели баннеры? Вы всегда можете отключить рекламу.
iНе можете найти то, что вам нужно? Попробуйте сервис подбора литературы.
i Надоели баннеры? Вы всегда можете отключить рекламу.

Текст научной работы на тему «Разработка учебного языка программирования и интерпретатора»

4. Грегер С.Э., Сковородин Е.Ю. Построение онтологического портала с использованием объектной базы // Объектные системы - 2010: Материалы I Международной научнопрактической конференции. Россия, Ростов-на-Дону, 10-12 мая 2010 г / под общ. ред. П.П. Олейника. - Ростов-на-Дону, 2010. - С. 74-78.

5. Грегер С.Э. Реализация инструментальной среды семантического моделирования учебного процесса. Объектные системы - 2011: материалы III Международной научно-практической конференции, (Ростов-на-Дону, 10-12 мая 2011 г.) / Под общ. ред. П.П. Олейника. - Ростов-на-Дону, 2011. - С.58-61.

УДК 004.4’23 +004.43

РАЗРАБОТКА УЧЕБНОГО ЯЗЫКА ПРОГРАММИРОВАНИЯ И ИНТЕРПРЕТАТОРА1

Лаптев Валерий Викторович, к.т.н., доцент, Астраханский государственный технический университет, Россия, Астрахань, [email protected] Грачев Дмитрий Александрович, магистрант, Астраханский государственный технический университет, Россия, Астрахань, [email protected]

Введение

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

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

• понятия языка для обучения должны соответствовать понятиям промышленных императивных языков программирования;

• множество понятий языка для обучения должно быть минимально;

• конструкции языка не должны зависеть ни от аппаратной платформы, ни от операционной системы;

• язык должен поддерживать модульность, процедурное программирование и объектно-ориентированное программирование;

• язык должен поддерживать структурное программирование;

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

В качестве примеров запрета умолчаний можно привести следующие:

• отсутствие неявных преобразований типа (кроме преобразования в арифметических выражениях целый -> вещественный);

• явный параметр this, задаваемый при определении метода класса;

• явное определение записи как базовой для наследования;

• явное указание типа константы при определении.

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

• базовая лексика языка должна быть русскоязычной;

• ключевые слова должны иметь английский эквивалент;

1 Лауреат номинации "Лучший доклад о методах преподавания объектных технологий в ВУЗе". Автор доклада награждается правом бесплатной публикации одного доклада по данной тематике на следующей конференции

92

• каждый оператор языка начинается ключевым словом;

• блочные конструкции языка завершаются ключевым словом «конец».

Основу реализации интерпретатора учебного языка составляют следующие принципы:

• эффективность выполнения является не очень важной;

• загрузка и связывание модулей должны выполняться динамически;

• управление памятью осуществляет система (сборка мусора);

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

• среда должна обеспечивать работу как с одномодульными, так и с многомодульными программами;

• ввод-вывод данных должен осуществляться в рамках среды без выхода в операционную систему;

• редактор кода, с одной стороны, должен обеспечивать традиционные операции редактирования текста, и, с другой стороны, должен оперировать семантическими конструкциями языка;

• изменение ключевых слов в коде должно быть невозможно;

• ошибки должны определяться в момент набора программы;

• переключение с русской лексики на английскую и обратно не должно приводить к повторной трансляции программы;

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

1. Учебный язык программирования

В языке определено всего 4 элементарных типа данных: вещественные числа, целые числа (со знаком), булевские и символьные. Для чисел определены традиционные арифметические операции с традиционными приоритетами. Для целых чисел определено целое деление («\»), отличающееся от вещественного деления, и операция получения остатка от деления («%»).

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

Для чисел определены также 6 традиционных операций сравнения: равно («=»), не равно (решетка - «#»), меньше («<»), больше («>»), больше или равно («>=»), меньше или равно («<=»). Для булевских данных определены булевские константы true (истина, да) и false (ложь, нет) и традиционные логические операции: not («~»), and («&»), or («|»), xor (исключающее или - «Л»), - с традиционными приоритетами. Для символьных данных не определено никаких операций.

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

Указатель определен с целью изучения динамических структур данных. Значением указателя может быть только адрес объекта определяемого типа и никакого другого.

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

93

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

Доступ к элементам массива осуществляются по индексу. Индекс - это целочисленное выражение и задается в квадратных скобках после имени массива. Элементы массива нумеруются, начиная с нуля до (размер-1). Индекс проверяется на корректность при обращении к элементу массива.

Массивы в языке SL являются одномерными, поэтому многомерный массив трактуется как «массив массивов массивов ...». Операции с объектами-массивами отсутствуют.

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

В языке определен единственный оператор, позволяющий конструировать новые типы данных. Определяемый тип - это тип данных, который изначально отсутствует в языке программирования и определяется программистом. Определяемый тип конструируется с помощью блочного (см. ниже) оператора «тип». Этот оператор является аналогом записи (структуры, класса) в промышленных языках программирования. В нем указывается имя нового типа, которое связывается с определяемой структурой данных. В типе задаются необходимые поля и методы.

Тело определяемого типа - это пространство имен, в котором все имена должны быть различны (тем самым перегрузка методов в одном классе не разрешена). Элементы по умолчанию являются приватными. Любой элемент можно определить как видимый на уровне текущего модуля, или как публичный, видимый во всех модулях, импортирующих данный. Поля определяемого типа можно объявить с модификатором readonly (только для чтения).

Только объекты определяемого типа могут создаваться динамически. Новый тип может быть определен только в модуле, и имя типа видно с момента объявления. В языке SL не существует никаких операций с объектами определяемых типов, кроме присваивания.

Операторы в языке SL делятся на два вида: простые и блочные. Блочные операторы начинаются с заголовка и заканчиваются ключевым словом «конец». Блочные операторы содержат последовательность операторов внутри себя. Эта последовательность операторов называется телом блочного оператора. Блочными операторами являются операторы цикла, условные операторы, операторы определения подпрограмм, оператор определения модуля и оператор определения типа.

Простые операторы тела не имеют. Простые операторы также начинаются ключевым словом, но заканчиваются символом «точка с запятой» («;»). К простым относятся операторы объявления констант и переменных, оператор присваивания, операторы ввода-вывода, оператор вызова процедуры, оператор возврата из подпрограммы, оператор импорта.

Объявление константы присваивает имя (идентификатор) некоторому значению. Оператор начинается ключевым словом «константа», и для каждой константы необходимо указывать тип, имя и выражение, значение которого и связывается с заданным именем. В языке SL разрешено объявлять константы только элементарных типов. Аналогичный синтаксис имеет оператор определения переменных, в котором вместо ключевого слова «константа» задается слово «переменная». В качестве типов могут использоваться все описанные выше типы, синтаксис которых можно представить следующим набором правил.

Только переменные элементарных типов разрешается инициализировать явно. При отсутствии инициализаторов переменные числовых типов обнуляются, а логическим присваивается false. Указатели и переменные процедурных типов при объявлении инициализируются системой специальным значением null. Для элементов массивов действуют те же правила.

94

Оператор присваивания начинается ключевым словом «присвоить» и имеет традиционный вид: слева переменная (элемент массива, поле записи), справа - совместимое по присваиванию выражение.

Оператор вызова процедуры начинается ключевым словом «вызвать», за которым следует имя процедуры и список параметров.

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

Условный оператор является блочным и имеет традиционный вид: начинается ключевым словом «если» и заканчивается ключевым словом «конец». В тело оператора может быть вставлена одна ветвь «иначе» и произвольное число ветвей «а_если» (в английской нотации «else_if»).

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

if

| охрана: операторы;

| охрана: операторы;

else

операторы;

end;

Формат этого оператора совпадает с форматом условного оператора Дейкстры [2], однако семантика существенно отличается. Оператор Дейкстры по определению является недетерминированным (одна из охран выбирается случайным образом), что неприемлемо при обучении практическому программированию. В нашем варианте охраны (булевские выражения) не являются взаимоисключающими, и выполняются те группы операторов, значения охран которых истинны. Если все охраны ложны, то выполняется ветка else (которая может отсутствовать).

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

В учебный язык включен только один традиционный оператор цикла - цикл while. C помощью этого цикла моделируются другие формы операторов цикла. В экспериментальных целях (для изучения удобства использования) в SL определен цикл Дейкстры [2], который имеет следующий вид в английской нотации:

do

| охрана: операторы;

| охрана: операторы;

| охрана: операторы;

end;

Как описано в [2], охраны (булевские выражения) не являются взаимоисключающими, и выполняются те группы операторов, значения охран которых истинны. Цикл выполняется до тех пор, пока хотя бы одна охрана истинна.

Для поддержки процедурной парадигмы в языке определены процедуры и функции, которые традиционно содержат заголовок и тело:

процедура (параметры) имя операторы;

конец имя;

Функция отличается заголовком, в котором указывается возвращаемый тип:

функция (параметры):тип имя

95

Возврат из процедур осуществляется оператором «вернуть» без аргумента. Этот же оператор с аргументом-выражением используется для возврата результата из функций.

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

Типы параметров и возвращаемого результата могут быть любыми допустимыми типами. Параметры могут быть входными, выходными или переменными.

Методы могут быть определены только в теле определяемого типа. В заголовке метода явно указывается дополнительный параметр - аналог невидимого параметра this в C-подобных объектно-ориентированных языках.

Для поддержки модульности в учебном языке определена явная конструкция модуля, аналогичная конструкции модуля в языке Компонентный Паскаль [3]. Модуль должен иметь уникальное имя и включает две секции: секцию определений и секцию инициализации. В секции определений задаются список импортируемых модулей (если он есть), определение типов, констант, переменных, и независимых процедур и функций.

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

Выполняемой программой является модуль. В секции инициализации модуля задается последовательность операторов, которые выполняются при загрузке модуля в память. Секция инициализации - это неименованная процедура, выполняемая первой. Объявленные в ней переменные и константы являются локальными. В этой секции может быть задан вызов любой процедуры, функции или метода, определённых в секции объявлений. Выполнение операторов секции начинается после загрузки импортированных модулей.

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

2. Примеры использования учебного языка программирования

Рассмотрим несколько простых примеров программ на учебном языке. Как уже принято в компьютерном мире, первая программа - HelloWorld.

# Первая программа на учебном языке - это комментарий #

модуль HelloWorld

начало

вывести "Привет, Мир!"; конец HelloWorld.

Второй пример - вычисление факториала (в английской нотации).

module Factorial begin

variable integer i := 1; variable integer current := 1; constant integer N = 20;

iНе можете найти то, что вам нужно? Попробуйте сервис подбора литературы.

while i < N do

let current := current * i; let i := i + 1; output current; output '\n'; end while; end Factorial.

Третий пример - модуль, в котором реализована простая функция для вычисления случайных чисел.

96

модуль Рандом переменная целое х := 5;

# открытая функция - доступна в других модулях #

открыт функция (входной целое мин, входной целое макс):целое рандом константа целое a := 2147483647; константа целое b := 48271; константа целое c := 44488; константа целое d := 3399;

присвоить х := b * (x % c) - d * (x \ c); если х < 0 тогда

присвоить х := х + а; конец ветвления;

вернуть х % (макс - мин) + мин; конец рандом; начало

конец Рандом.

Использование модуля Рандом показывает следующий пример, в котором массив заполняется случайными числами и выводится.

модуль СлучайныйМассив подключить Рандом;

константа целое размер := 10; переменная массив [размер] целое а;

процедура () ИнициализацияМассива переменная целое и := 0; пока и < размер повторять

присвоить а[и] := Рандом.рандом(0, 50); присвоить и := и + 1; конец цикла;

конец ИнициализацияМассива;

процедура () ВывестиМассив переменная целое и := 0; пока и < размер повторять вывести а[и]; вывести '\t'; присвоить и := и + 1; конец цикла; конец ВывестиМассив; начало

вызвать ИнициализацияМассива(); вызвать ВывестиМассив(); конец СлучайныйМассив.

3. Семантический редактор

Для набора и выполнения программ на учебном языке была разработана интегрированная среда Semantic IDE, состоящая из семантического редактора и интерпретатора. Внешний вид среды показан на рис. 1.

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

Редактор кода - это не традиционный текстовый редактор. В Semantic IDE реализован семантический редактор. Как было описано в работе [1], семантический редактор оперирует не символами, а операторами учебного языка. Поэтому, во-первых, большинство элементов оператора сразу вставляются в код в правильном виде, и, во-вторых, полностью исчезают

97

ошибки набора ключевых слов. Редактор позволяет символьный ввод только в строго определенных позициях оператора. Например, в операторе «переменная» разрешено вводить посимвольно только имя переменной.

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

Рис. 1 - Внешний вид Semantic IDE

Редактор следит за действиями программиста и сообщает об ошибках в момент набора программы. Например, при ошибке в написании имени переменной в выражении в окне ошибок мгновенно появляется сообщение о том, что данное имя переменной не описано.

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

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

Помимо кода, редактор позволяет набирать любой текст в узлах-комментариях. Комментарий может быть написан как в коде программы, так и вне его. Кроме того, в комментарий можно вставлять рисунки и любой текст из буфера обмена. Шрифт комментария можно настраивать обычным для системы Windows образом. Все это позволяет непосредственно в редакторе среды готовить обучающие материалы, причем в документе может содержаться код примера, который можно выполнить.

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

4. Интерпретатор

Результатом работы редактора кода является семантическое дерево программы (см. рис. 2), которое является входным для интерпретатора. Каждый узел дерева представляет отдельный оператор программы (или комментарий). В каждом узле две ссылки: на

98

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

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

метод (Узел этот) Интерпретировать)) если

|этот.Дочерний # пусто():

вызвать этот.Дочерний.Интерпретировать();

|этот.Следующий # пусто():

вызвать этот. Следующий.Интерпретировать(); конец ветвления; вызвать память.СобратьМусор();

конец Интерпретировать;

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

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

1. Выполнение действий, характерных для данного оператора.

2. Проверка на наличие дочернего элемента. В случае его существования - запуск процедуры интерпретации у дочернего элемента.

3. Проверка на наличие следующего элемента. В случае его существования - запуск процедуры интерпретации у следующего элемента. Иначе - очистка памяти от объектов, вышедших из области видимости.

99

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

5. В случае возникновения ошибки процесс интерпретации прерывается, и управление передается среде. Процесс интерпретации также прерывается при интерпретации оператора, помеченного как «остановочный».

Данная схема характерна для большинства операторов (табл. 1), за исключением оператора условного перехода и оператора цикла.

Таблица 1. Операторы языка Semantic Language

Название Механизм интерпретации

переменная, инициализированная переменная, константа, массив, поле Создают соответствующий объект в памяти

тип Не интерпретируется. Представляется как класс в памяти программы на этапе семантического анализа.

вызвать Производит интерпретацию процедуры, указанную пользователем.

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

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

модуль Не интерпретируется.

начало Интерпретируется как вызов начальной (главной) процедуры

комментарий Использует базовый метод интерпретации.

иначе Использует базовый метод интерпретации.

функция, процедура, метод Создают локальные переменные или псевдонимы для параметров. Контролируют уровень стека памяти.

ввести Прерывает выполнение программы и запрашивает ввод пользователя. После ввода продолжает выполнение программы.

вывести Выводит строку на консоль.

Оператор условного перехода «если» вычисляет значение логического выражения, объявленного в условии, и в случае ложного выражения передает управление дочернему оператору оператора «иначе». Если оператор «иначе» отсутствует, то управление передается согласно стандартной схеме интерпретации.

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

5. Организация памяти

Виртуальная машина для языка SL имеет стековую архитектуру. Стек памяти разделен на кадры, каждый из которых имеет порядковый номер. Кадр стека с номером 0 является глобальной областью памяти, которая создается при запуске программы и живет до конца ее выполнения. В ней хранятся все глобальные объекты, объявленные в программе. При вызове подпрограммы в стеке создается новый кадр со значением на единицу больше предыдущего, при выходе из подпрограммы этот кадр удаляется. Однако ссылочные переменные (например, объекты типов) остаются в памяти. Это позволяет применить ссылочную парадигму без указателей (как, например, в языках C# и Java), реализовав сборку мусора.

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

100

объектов в программе с объектами в памяти. При обращении к объекту по его имени сначала вычисляется ссылка, а потом, если это необходимо, возвращается объект. Вопрос о необходимости возвращения непосредственно объекта или ссылки на него в операции присваивания открывает интересный аспект архитектуры чисто интерпретируемого языка.

Разные промышленные языки решают эту проблему только одним из двух способов. Например, C++ копирует объекты, а C# и Java - ссылки. Это решение зафиксировано в реализации трансляторов и в реализации виртуальной машины языка. Заметим, что в императивных языках обычно можно реализовать альтернативный режим копирования вручную, написав соответствующий код.

При использовании чистого интерпретатора возникает возможность в рамках одной реализации использовать оба подхода. Пользователь устанавливает (средствами среды) некоторый флаг. Если пользователь решил использовать копируемый подход, то интерпретатор работает в режиме копирования объектов. Отметим, что при этом можно реализовать разные режимы копирования: непосредственное копирование или «ленивое» копирование, при котором реальное копирование откладывается до того момента, когда реально потребуется копия объекта.

Если же пользователь решил использовать ссылочный подход, то интерпретатор работает в режиме копирования ссылок.

Таким образом, в рамках разрабатываемой среды Semantic IDE имеется возможность обучать программированию в двух противоположных парадигмах без написания кода просто переключением одного флажка!

Выводы

Описанная в данной статье интегрированная среда Semantic IDE является основой обучающей системы, требования к которой изложены в [1]. В настоящее время реализация Semantic IDE выполняется на языке C# в среде Visual Studio 2010. Уже полностью реализована вся процедурная часть учебного языка. В 2011 году Дмитрий Грачев с описанной системой выиграл грант по программе «У.М.Н.И.К.».

Небольшой пока опыт использования показывает существенное сокращение времени по созданию кода программы и практически полное исчезновение ошибок набора. В настоящий момент развитие среды продолжается: реализуется объектно-ориентированная часть Semantic Language и модули системной библиотеки. Предполагается внедрение системы в Астраханском государственном техническом университете уже в новом 2012 учебном году.

Литература

1. Лаптев В.В. Требования к современной обучающей среде по программированию // Объектные

системы-2010 (Зимняя сессия): материалы II Международной научно-практической

конференции. Россия, Ростов-на-Дону, 10-12 ноября 2010 г. / Под общ. Ред. П.П. Олейника. -Ростов-на-Дону, 2010. - с. 104-110.

2. Дейкстра Э. Дисциплина программирования. - М.: Мир, 1978. - 275 с.

3. Потопахин В. Современное программирование с нуля! - М.: ДМК Пресс, 2010. - 240 с.

101

i Надоели баннеры? Вы всегда можете отключить рекламу.