Создание собственного языка программирования с нуля: Часть X — Обработка исключений
25 апреля 2023 г.Добро пожаловать в следующую часть создания собственного языка программирования. В этой части мы продолжим улучшать наш игрушечный язык, реализуя исключения. Вот предыдущие части:
- Создание собственного языка программирования с нуля ли>
- Построение Ваш собственный язык программирования с нуля: часть II — двухстековый алгоритм Дейкстры
- Сборка Ваш собственный язык программирования, часть III: Улучшение лексического анализа с помощью регулярных выражений Lookaheads
- Создание собственного языка программирования С нуля, часть 4: реализация функций
- Создание собственного языка программирования с нуля : Часть V. Массивы
- Создание собственного языка программирования с нуля : Часть VI. Петли
- Создание собственного языка программирования с нуля : Часть VII. Классы
- Создание собственного языка программирования С нуля: Часть VIII. Вложенные классы
- Создание собственного языка программирования с нуля: Часть IX. Гибридное наследование
Полный исходный код доступен на GitHub.
1. Модель исключений
Во-первых, мы определим синтаксические правила того, как мы будем генерировать и обрабатывать исключения, аналогичные синтаксису Ruby:
- Чтобы создать исключение, мы будем использовать ключевое слово
raise
:
raise
- Мы должны предоставить сообщение с дополнительной информацией об ошибке:
raise "This is an Exception"
- Мы можем указать ошибку как экземпляр класса или любого другого выражения:
class Exception [message]
end
raise new Exception ["This is an Exception message"]
- Чтобы предоставить более подробную информацию о возникшем исключении, мы соберем и распечатаем трассировку стека в виде списка инструкций, которые программа выполняла для достижения инструкции, вызывающей исключение:
1: do_something []
2:
3: fun do_something
4: new Test :: do_something_else []
5: end
6:
7: class Test
8:
9: fun do_something_else
10: do_even_more []
11: end
12:
13: fun do_even_more
14: raise "A message that describes the error."
15: end
16:
17: end
Вывод:
:::предупреждение Сообщение с описанием ошибки.
в Test#do_even_more:14
в Test#do_something_else:10
в do_something:4
на test.toy:1
:::
- Для обработки исключения мы будем использовать следующие блоки кода:
begin
# Statements raising an Exception
rescue
# Handle an Exception
ensure
# Always executed
end
- Чтобы получить доступ к сгенерированному исключению в блоке восстановления, мы можем объявить произвольную переменную после ключевого слова
rescue
:
begin
# Statements raising an Exception
rescue error
# Access and handle an Exception using `error` variable
print error
end
2. Лексический анализ
В этом разделе мы рассмотрим лексический анализ как первую стадию процесса компиляции, которая разделяет исходный код на языковые лексемы, такие как ключевое слово, переменная, оператор и т. д.
Чтобы определить лексемы в этой реализации игрушечного языка, я использую выражения регулярных выражений, перечисленные в TokenType перечисление:
package org.example.toylanguage.token;
...
public enum TokenType {
Comment("#.*"),
LineBreak("[nr]"),
Whitespace("[st]"),
Keyword("(if|elif|else|end|print|input|class|fun|return|loop|in|by|break|next|assert)(?=s|$)(?!_)"),
GroupDivider("([|]|,|{|}|.{2}|(:(?!:)))"),
Logical("(true|false)(?=s|$)(?!_)"),
Numeric("([-]?(?=[.]?[0-9])[0-9]*(?![.]{2})[.]?[0-9]*)"),
Null("(null)(?=,|s|$)(?!_)"),
This("(this)(?=,|s|$)(?!_)"),
Text(""([^"]*)""),
Operator("(+|-|*{1,2}|/{1,2}|%|>=|>|<=|<{1,2}|={1,2}|!=|!|:{2}s+new|:{2}|(|)|(new|and|or|as|is)(?=s|$)(?!_))"),
Variable("[a-zA-Z_]+[a-zA-Z0-9_]*");
...
}
Каждая строка кода на игрушечном языке обрабатывается с помощью этих регулярных выражений и с помощью LexicalParser, мы преобразуем исходный код в Token лексемы. Чтобы разобрать новые слова, объявленные в правилах исключений (raise
, begin
, rescue
, ensure
), нам нужно добавьте их в регулярное выражение лексемы Keyword
:
...
public enum TokenType {
...
Keyword("(if|elif|else|end|print|input|class|fun|return|loop|in|by|break|next|assert|raise|begin|rescue|ensure)(?=s|$)(?!_)"),
...
}
3. Синтаксический анализ
В этом разделе мы преобразуем лексемы, полученные в результате лексического анализа, в окончательные утверждения.
3.1 Преобразование лексем в операторы
Чтобы преобразовать объявленные лексемы Keyword
в операторы, нам нужно определить операторы. Для реализации оператора мы используем оператор. Реализованный RaiseExceptionStatement
должен содержать поднятое выражение:
package org.example.toylanguage.statement;
@RequiredArgsConstructor
@Getter
public class RaiseExceptionStatement implements Statement {
private final Expression expression;
@Override
public void execute() {
// TODO raise an Exception
}
}
Оператор для обработки Exception также будет реализовывать интерфейс Statement, но в отличие от RaiseExceptionStatement
, он должен включать вложенные операторы для каждого из этих трех блоков:
begin
# Begin block
rescue error
# Rescue block
ensure
# Ensure block
end
Каждое из этих утверждений является реализацией CompositeStatement, будет содержать вложенные операторы внутри себя, что позволяет выполнять несколько операторов. Чтобы записать исключение в переменную ошибки для блока rescue
, мы определим errorVariable
как строковое поле:
package org.example.toylanguage.statement;
@RequiredArgsConstructor
@Getter
public class HandleExceptionStatement implements Statement {
private final CompositeStatement bodyStatement;
private final CompositeStatement rescueStatement;
private final CompositeStatement ensureStatement;
private final String errorVariable;
@Override
public void execute() {
// TODO handle an Exception
}
}
Следующим шагом является преобразование маркеров исключения в реализации RaiseExceptionStatement
и HandleExceptionStatement
. Чтобы преобразовать токены в операторы, мы используем StatementParser< /а>. В этом конкретном случае, чтобы преобразовать токен ключевого слова, нам нужно изменить StatementParser#parseKeywordStatement(Token token), который анализирует оператор в зависимости от первого слова, которое начинается с оператора .
Давайте добавим новые первые слова в блок переключателей для создания и обработки исключения: raise
для RaiseExceptionStatement и begin
для HandleExceptionStatement:
package org.example.toylanguage;
public StatementParser {
...
private void parseKeywordStatement(Token token) {
switch (token.getValue()) {
...
case "raise":
parseRaiseExceptionStatement();
break;
case "begin":
parseHandleExceptionStatement();
break;
default:
throw new SyntaxException(String.format("Failed to parse a keyword: %s", token.getValue()));
}
}
...
}
Чтобы создать RaiseExceptionStatement
, нам нужно только прочитать вызываемое выражение. Для чтения выражений мы используем ExpressionReader. , который анализирует полное выражение, пока не достигнет начала следующего оператора:
private void parseRaiseExceptionStatement() {
Expression expression = ExpressionReader.readExpression(tokens);
...
}
После создания RaiseExceptionStatement
нам нужно добавить его в StatementParser#compositeStatement
как вложенный оператор во внешний оператор:
private void parseRaiseExceptionStatement() {
Expression expression = ExpressionReader.readExpression(tokens);
RaiseExceptionStatement statement = new RaiseExceptionStatement(expression);
compositeStatement.addStatement(statement);
}
Чтобы создать HandleExceptionOperator
, нам нужно прочитать три блока: begin (body)
, rescue
и ensure
, а не забыв прочитать слово end
в конце, которое обозначает конец оператора обработки исключений:
private void parseHandleExceptionStatement() {
// read begin block
CompositeStatement beginStatement = ...;
// read rescue block
CompositeStatement rescueStatement = ...;
String errorVariable = ...;
// read ensure block
CompositeStatement ensureStatement = ..;
// skip the end keyword
tokens.next(TokenType.Keyword, "end");
// construct a statement
HandleExceptionStatement statement = new HandleExceptionStatement(beginStatement, rescueStatement, ensureStatement, errorVariable);
compositeStatement.addStatement(statement);
}
Начнем с блока begin
. Чтобы проанализировать вложенные операторы внутри него, нам нужно будет использовать метод StatementParser#parse(StatementParser, CompositeStatement, DefinitionScope)
. В качестве первого аргумента он принимает экземпляр StatementParser внешнего блока кода. Второй аргумент — это CompositeStatement, в котором будут собраны все вложенные операторы в анализируемом блоке. Третий аргумент — DefinitionScope< /a>, который используется для записи всех структур (классов и функций), объявленных внутри анализируемого блока. Если мы хотим ограничить доступ к структурам, объявленным внутри начального блока, из внешнего блока, мы должны открыть новый DefinitionScope:
// read begin block
CompositeStatement beginStatement = new CompositeStatement();
DefinitionScope beginScope = DefinitionContext.newScope();
StatementParser.parse(this, beginStatement, beginScope);
Метод StatementParser#parse(StatementParser, CompositeStatement, DefinitionScope)
будет считывать все вложенные операторы, пока мы не достигнем завершающего слова, обозначающего конец этого блока. В настоящее время, чтобы проверить, встретили ли мы завершающее слово, мы используем StatementParser#hasNextStatement()
. Давайте добавим новые слова rescue
и ensure
, чтобы гарантировать остановку синтаксического анализа операторов при встрече с этими блоками:
public class StatementParser {
...
private boolean hasNextStatement() {
if (!tokens.hasNext())
return false;
if (tokens.peek(TokenType.Operator, TokenType.Variable, TokenType.This))
return true;
if (tokens.peek(TokenType.Keyword)) {
return !tokens.peek(TokenType.Keyword, "elif", "rescue", "ensure", "else", "end");
}
return false;
}
...
}
Далее давайте прочитаем второй блок rescue
. Он может отсутствовать, если пользователь не хочет перехватывать и обрабатывать исключение:
// read rescue block
CompositeStatement rescueStatement = ...;
String errorVariable = ...;
if (tokens.peek(TokenType.Keyword, "rescue")) {
tokens.next(); // skip rescue word
}
Прежде чем читать вложенные операторы, давайте проверим, указал ли пользователь переменную для ссылки на возбужденное исключение:
// read rescue block
CompositeStatement rescueStatement = ...;
String errorVariable = null;
if (tokens.peek(TokenType.Keyword, "rescue")) {
tokens.next(); // skip rescue word
if (tokens.peekSameLine(TokenType.Variable)) {
Token error = tokens.next();
errorVariable = error.getValue();
}
}
Теперь давайте прочитаем вложенные операторы, как мы ранее читали операторы begin:
// read rescue block
CompositeStatement rescueStatement = null;
String errorVariable = null;
if (tokens.peek(TokenType.Keyword, "rescue")) {
tokens.next(); // skip rescue word
if (tokens.peekSameLine(TokenType.Variable)) {
Token error = tokens.next();
errorVariable = error.getValue();
}
rescueStatement = new CompositeStatement();
DefinitionScope rescueScope = DefinitionContext.newScope();
StatementParser.parse(this, rescueStatement, rescueScope);
}
И, наконец, давайте закончим третий блок ensure
. Это может быть необязательным блоком rescue
:
// read ensure block
CompositeStatement ensureStatement = null;
if (tokens.peek(TokenType.Keyword, "ensure")) {
tokens.next(); // skip rescue word
ensureStatement = new CompositeStatement();
DefinitionScope ensureScope = DefinitionContext.newScope();
StatementParser.parse(this, ensureStatement, ensureScope);
}
3.2 Выполнение операторов исключений
3.2.1 RaiseExceptionStatement
Когда мы выполняем RaiseExceptionStatement, каждый из последующих операторов должен быть уведомлен о сбое программы, и выполнение должно быть остановлено. Чтобы разделить это событие между другими операторами, мы введем класс ExceptionContext
, который будет содержать сведения об исключении:
package org.example.toylanguage.context;
public class ExceptionContext {
@Getter
private static Exception exception;
private static boolean raised;
@RequiredArgsConstructor
@Getter
public static class Exception {
private final Value<?> value;
@Override
public String toString() {
return value.toString();
}
}
}
Класс Exception предоставит подробную информацию о возникшем исключении, включая записи о перемещении приложения внутри него для печати трассировки стека.
Далее мы добавим несколько методов для создания и обработки исключения:
public class ExceptionContext {
...
public static void raiseException(Value<?> value) {
exception = new Exception(value);
raised = true;
}
public static void rescueException() {
exception = null;
raised = false;
}
public static boolean isRaised() {
return raised;
}
}
Затем давайте завершим RaiseExceptionStatement#execute()
и уведомим другие операторы с помощью ExeceptionContext
:
package org.example.toylanguage.statement;
public class RaiseExceptionStatement implements Statement {
private final Expression expression;
@Override
public void execute() {
Value<?> value = expression.evaluate();
ExceptionContext.raiseException(value);
}
}
Если пользователь не указал выражение ошибки, мы можем вывести текстовое выражение по умолчанию:
public class RaiseExceptionStatement implements Statement {
private final Expression expression;
@Override
public void execute() {
Value<?> value = expression.evaluate();
if (value == NullValue.NULL_INSTANCE) {
value = new TextValue("Empty exception");
}
ExceptionContext.raiseException(value);
}
}
Зная, что ExceptionContext будет уведомлен о поднятом Exception, мы должны проверить, что никакие последующие операторы не будут выполняться, если Exception возникнет. В настоящее время все операторы в любом блоке кода выполняются реализациями CompositeStatement
. Для каждой реализации CompositeStatement
, в которой мы повторяем вложенные операторы с помощью CompositeStatement#statements2Execute
, нам необходимо установить проверку после каждого выполняемого оператора в случае возникновения исключения, а в положительном случае остановить исполнение:
package org.example.toylanguage.statement;
@Getter
public class CompositeStatement implements Statement {
...
@Override
public void execute() {
for (Statement statement : statements2Execute) {
statement.execute();
// stop the execution in case Exception occurred
if (ExceptionContext.isRaised())
return;
//stop the execution in case ReturnStatement is invoked
if (ReturnContext.getScope().isInvoked())
return;
}
}
}
package org.example.toylanguage.statement.loop;
public abstract class AbstractLoopStatement implements CompositeStatement {
...
@Override
public void execute() {
...
try {
...
while (hasNext()) {
...
try {
// execute inner statements
for (Statement statement : getStatements2Execute()) {
statement.execute();
// stop the execution in case Exception occurred
if (ExceptionContext.isRaised())
return;
// stop the execution in case ReturnStatement is invoked
if (ReturnContext.getScope().isInvoked())
return;
// stop the execution in case BreakStatement is invoked
if (BreakContext.getScope().isInvoked())
return;
// jump to the next iteration in case NextStatement is invoked
if (NextContext.getScope().isInvoked())
break;
}
} finally {
NextContext.reset();
MemoryContext.endScope(); // release each iteration memory
...
}
}
} finally {
MemoryContext.endScope(); // release loop memory
BreakContext.reset();
}
}
}
При установке этих изменений операторы прекратят выполнение после поднятого оператора Exception. В конце выполнения программы мы должны, если возникло исключение, и напечатать сообщение об исключении:
package org.example.toylanguage;
public class ToyLanguage {
@SneakyThrows
public void execute(Path path) {
String source = Files.readString(path);
LexicalParser lexicalParser = new LexicalParser(source);
List<Token> tokens = lexicalParser.parse();
DefinitionContext.pushScope(DefinitionContext.newScope());
MemoryContext.pushScope(MemoryContext.newScope());
try {
CompositeStatement statement = new CompositeStatement();
StatementParser.parse(tokens, statement);
statement.execute();
} finally {
DefinitionContext.endScope();
MemoryContext.endScope();
if (ExceptionContext.isRaised()) {
ExceptionContext.printStackTrace();
}
}
}
}
Чтобы распечатать Exception, мы будем использовать метод ExceptionContext#printStackTrace()
, который в дальнейшем также будет отображать записи о перемещении приложения:
public class ExceptionContext {
...
public static void printStackTrace() {
System.err.println(exception);
}
}
3.2.2. HandleExceptionStatement
Для обработки исключения завершим реализацию HandleExceptionStatement#execute()
. Он будет состоять из трех частей для каждого из определенных блоков:
public class HandleExceptionStatement implements Statement {
private final CompositeStatement beginStatement;
private final CompositeStatement rescueStatement;
private final CompositeStatement ensureStatement;
private final String errorVariable;
@Override
public void execute() {
//begin block
// rescue block
// ensure block
}
}
Каждый из блоков должен выполняться в новом MemoryScope, ограничивающий доступ к переменным, объявленным во вложенном блоке, из внешнего блока:
//begin block
MemoryContext.pushScope(MemoryContext.newScope());
try {
bodyStatement.execute();
} finally {
MemoryContext.endScope();
}
Блок rescue
является необязательным и должен выполняться только в том случае, если мы перехватили исключение в ExceptionContext
:
// rescue block
if (rescueStatement != null && ExceptionContext.isRaised()) {
MemoryContext.pushScope(MemoryContext.newScope());
try {
rescueStatement.execute();
} finally {
MemoryContext.endScope();
}
}
Если этот блок спасает исключение, мы должны сообщить ExceptionContext
, что ошибка обнаружена:
// rescue block
if (rescueStatement != null && ExceptionContext.isRaised()) {
MemoryContext.pushScope(MemoryContext.newScope());
ExceptionContext.rescueException();
try {
rescueStatement.execute();
} finally {
MemoryContext.endScope();
}
}
Наконец, для этого блока мы должны инициализировать переменную error, предоставленную пользователем, со значением Exception, полученным из ExceptionContext:
// rescue block
if (rescueStatement != null && ExceptionContext.isRaised()) {
MemoryContext.pushScope(MemoryContext.newScope());
if (errorVariable != null) {
MemoryContext.getScope().setLocal(errorVariable, ExceptionContext.getException().getValue());
}
ExceptionContext.rescueException();
try {
rescueStatement.execute();
} finally {
MemoryContext.endScope();
}
}
Третий блок ensure
также может быть необязательным, как и блок rescue
:
// ensure block
if (ensureStatement != null) {
MemoryContext.pushScope(MemoryContext.newScope());
try {
ensureStatement.execute();
} finally {
MemoryContext.endScope();
}
}
3.3* Добавление трассировки стека
В этом подразделе мы соберем записи о перемещении приложения во время его выполнения и отобразим полную трассировку стека для возникших исключений:
:::предупреждение Сообщение с описанием ошибки.
в Test#do_even_more:14
в Test#do_something_else:10
в do_something:4
на test.toy:1
:::
3.3.1 Определение оператора трассировки
Чтобы собрать трассировку стека, каждая из наших реализаций Statement должна содержать информацию об имени блока и номере строки. Мы можем преобразовать интерфейс Statement в абстрактный класс, определяющий эти два поля: blockName
и rowNumber
:
package org.example.toylanguage.statement;
@RequiredArgsConstructor
@Getter
public abstract class Statement {
private final Integer rowNumber;
private final String blockName;
public abstract void execute();
}
Доступ к rowNumber
можно получить из Токен, содержащий слово, обозначающее начало оператора:
public class StatementParser {
...
private void parseKeywordStatement(Token rowToken) {
switch (rowToken.getValue()) {
case "print":
parsePrintStatement(rowToken);
break;
case "input":
parseInputStatement(rowToken);
break;
case "if":
parseConditionStatement(rowToken);
break;
case "class":
parseClassDefinition(rowToken);
break;
case "fun":
parseFunctionDefinition(rowToken);
break;
case "return":
parseReturnStatement(rowToken);
break;
case "loop":
parseLoopStatement(rowToken);
break;
case "break":
parseBreakStatement(rowToken);
break;
case "next":
parseNextStatement(rowToken);
break;
case "assert":
parseAssertStatement(rowToken);
break;
case "raise":
parseRaiseExceptionStatement(rowToken);
break;
case "begin":
parseHandleExceptionStatement(rowToken);
break;
default:
throw new SyntaxException(String.format("Failed to parse a keyword: %s", rowToken.getValue()));
}
}
...
}
Структуры языка игрушек, которые у нас есть в настоящее время, — это классы и функции.
Чтобы установить ClassStatement#blockName
, мы можем использовать имя класса, полученное из ClassDetails#getName()
:
public class StatementParser {
...
private void parseClassDefinition(Token rowToken) {
// read class details
ClassDetails classDetails = readClassDetails();
...
// add class definition
...
ClassStatement classStatement = new ClassStatement(rowToken.getRow(), classDetails.getName());
...
//parse class's statements
...
}
...
}
Чтобы установить FunctionStatement#blockName
, мы можем использовать имя функции. В дополнение к имени мы можем указать имя класса, если функция объявлена внутри класса:
public class StatementParser {
...
private void parseFunctionDefinition(Token rowToken) {
Token type = tokens.next(TokenType.Variable);
...
//add function definition
String blockName = type.getValue();
if (compositeStatement instanceof ClassStatement) {
blockName = compositeStatement.getBlockName() + "#" + blockName;
}
FunctionStatement functionStatement = new FunctionStatement(rowToken.getRow(), blockName);
...
}
...
}
Другие операторы не определяют структуры и могут повторно использовать имена блоков классов и функций, ссылаясь на внешний блок кода с помощью StatementParser#compositeStatement#getBlockName()
, например:
public class StatementParser {
...
private void parsePrintStatement(Token rowToken) {
...
PrintStatement statement = new PrintStatement(rowToken.getRow(), compositeStatement.getBlockName(), expression);
...
}
...
private void parseInputStatement(Token rowToken) {
...
InputStatement statement = new InputStatement(rowToken.getRow(), compositeStatement.getBlockName(), variable.getValue(), scanner::nextLine);
...
}
...
}
При текущем способе создания корневого CompositeStatement мы должны указать корневое имя в ToyLanguage, который может быть определен как имя файла:
public class ToyLanguage {
@SneakyThrows
public void execute(Path path) {
String sourceCode = Files.readString(path);
List<Token> tokens = LexicalParser.parse(sourceCode);
DefinitionContext.pushScope(DefinitionContext.newScope());
MemoryContext.pushScope(MemoryContext.newScope());
try {
CompositeStatement statement = new CompositeStatement(null, path.getFileName().toString());
StatementParser.parse(tokens, statement);
statement.execute();
} finally {
DefinitionContext.endScope();
MemoryContext.endScope();
if (ExceptionContext.isRaised()) {
ExceptionContext.printStackTrace();
}
}
}
}
3.3.2 Сбор трассировки стека
Теперь каждый оператор содержит имя блока и номер строки. Давайте добавим набор операторов в ExceptionContext#Exception
:
public class ExceptionContext {
@Getter
private static Exception exception;
private static boolean raised;
public static boolean raiseException(Value<?> value) {
exception = new Exception(value, new Stack<>());
raised = true;
}
public static void rescueException() {
exception = null;
raised = false;
}
public static boolean isRaised() {
return raised;
}
public static void addTracedStatement(Statement statement) {
if (isRaised()) {
exception.stackTrace.add(statement);
}
}
public static void printStackTrace() {
System.err.println(exception);
rescueException();
}
@RequiredArgsConstructor
@Getter
public static class Exception {
private final Value<?> value;
private final List<Statement> stackTrace;
@Override
public String toString() {
return String.format("%s%n%s",
value,
stackTrace
.stream()
.map(st -> String.format("%4sat %s:%d", "", st.getBlockName(), st.getRowNumber()))
.collect(Collectors.joining("n"))
);
}
}
}
ExceptionContext#addTracedStatement(Statement)
должен вызываться каждым выражением, содержащим выражение, после вызова Expression#evaluate()
:
package org.example.toylanguage.statement;
public class ExpressionStatement extends Statement {
...
@Override
public void execute() {
expression.evaluate();
ExceptionContext.addTracedStatement(this);
}
}
package org.example.toylanguage.statement;
public class PrintStatement extends Statement {
...
@Override
public void execute() {
Value<?> value = expression.evaluate();
System.out.println(value);
ExceptionContext.addTracedStatement(this);
}
}
package org.example.toylanguage.statement;
public class RaiseExceptionStatement extends Statement {
...
@Override
public void execute() {
Value<?> value = expression.evaluate();
if (value == NullValue.NULL_INSTANCE) {
value = new TextValue("Empty exception");
}
ExceptionContext.raiseException(value);
ExceptionContext.addTracedStatement(this);
}
}
package org.example.toylanguage.statement;
public class ReturnStatement extends Statement {
...
@Override
public void execute() {
Value<?> result = expression.evaluate();
ReturnContext.getScope().invoke(result);
ExceptionContext.addTracedStatement(this);
}
}
4 Подведение итогов
В этой части мы реализовали простую модель для создания и обработки исключений. Еще один шаг к созданию полноценного языка программирования. Вот несколько примеров, которые вы можете запустить: raise_exception.toy и handle_exception.toy.
Оригинал