Создание собственного языка программирования с нуля: Часть IX — Гибридное наследование
25 марта 2023 г.Добро пожаловать в следующую часть создания собственного языка программирования. В этой части мы продолжим совершенствовать наш игрушечный язык, реализовав наследование для представленных ранее классов. Вот предыдущие части:
- Создание собственного языка программирования с нуля ли>
- Построение Ваш собственный язык программирования с нуля: часть II — двухстековый алгоритм Дейкстры
- Сборка Ваш собственный язык программирования, часть III: Улучшение лексического анализа с помощью регулярных выражений Lookaheads
- Создание собственного языка программирования С нуля, часть 4: реализация функций
- Создание собственного языка программирования с нуля : Часть V. Массивы
- Создание собственного языка программирования с нуля : Часть VI. Петли
- Создание собственного языка программирования с нуля : Часть VII. Классы
- Создание собственного языка программирования с нуля: Часть VIII - Вложенные классы
Полный исходный код доступен на GitHub.
1. Модель наследования
Начнем с определения правил наследования для наших классов:
- Чтобы наследовать класс, мы будем использовать символ двоеточия
:
, аналогичный синтаксису C++:
class Base
end
class Derived: Base
end
- У нас должна быть возможность заполнить свойства базового класса свойствами производного класса:
class Base [base_arg]
end
class Derived [derived_arg1, derived_arg2]: Base [derived_arg2]
end
- Когда мы создаем экземпляр класса Derived, содержащий операторы конструктора, сначала должны выполняться операторы, объявленные в конструкторе Base класса:
class Base
print "Constructor of Base class called"
end
class Derived: Base
print "Constructor of Derived class called"
end
d = new Derived
Вывод:
Constructor of Base class called
Constructor of Derived class called
- Наши классы будут поддерживать гибридное наследование и создавать классы из нескольких типов:
class A
end
class B
end
class C
end
class Derived: A, B, C
end
- Чтобы выполнить операцию Upcasting или Downcasting для экземпляра класса, мы будем использовать оператор
as
:
class Base
end
class Derived: Base
end
d = new Derived
b = d as Base # Downcasting to Base
d2 = b as Derived # Upcasting to Derived
- Чтобы изменить свойства класса Base, мы должны сначала привести объект к типу Base:
class Base [base_arg]
end
class Derived [derived_arg]: Base [derived_arg]
end
d = new Derived [ 1 ]
# Directly changing a property of the Derived type
d :: derived_arg = 2
# Downcasting instance to the Base and then changing Base’s property
d as Base :: base_arg = 3
- Если мы изменим свойство в базовом классе, соответствующее ссылочное свойство в производном классе также должно быть обновлено, и наоборот, если мы изменим < strong>Производное свойство класса, которое мы использовали для создания базового класса, соответствующее свойство в этом базовом классе также должно быть обновлено:
class Base [base_arg]
end
class Derived [derived_arg]: Base [derived_arg]
end
d = new Derived [ 1 ]
d as Base :: base_arg = 2
print d
d :: derived_arg = 3
print d as Base
Вывод:
Derived [ derived_arg = 2 ]
Base [ base_arg = 3 ]
- Мы не будем использовать ключевое слово
super
, которое есть в Java, потому что при гибридном наследовании может быть множество унаследованных базовых классов, и нет никакой возможности чтобы узнать, на какой супер класс ссылаться, не определяя его явным образом.
Для такого рода действий мы будем использовать оператор приведения, чтобы указать требуемый тип Base:
class A
fun action
print "A action"
end
end
class B
fun action
print "B action"
end
end
class C: A, B
fun action
this as B :: action []
this as A :: action []
print "C action"
end
end
c = new C
c :: action []
Вывод:
B action
A action
C action
- Наконец, в этой части мы добавим оператор
is
, чтобы проверить, является ли объект экземпляром определенного класса или нет:
class A
end
class B: A
end
fun check_instance [object]
if object is B
print "Object is type of B"
elif object is A
print "Object is type of A"
end
end
check_instance [ new A ]
check_instance [ new B ]
Вывод:
Object is type of A
Object is type of B
Теперь с этими определенными правилами давайте реализуем модель наследования, используя уже созданные структуры из предыдущих частей. Как обычно, мы рассмотрим два основных раздела: лексический анализ и синтаксический анализ.
2. Лексический анализ
В этом разделе мы рассмотрим лексический анализ. Это процесс разделения исходного кода на языковые лексемы, такие как ключевые слова, переменные, операторы и т. д. Чтобы определить лексемы, я использую выражения регулярных выражений, перечисленные в TokenType enum.
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)(?=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}|!=|!|:{2}s+new|:{2}|(|)|(new|and|or)(?=s|$))(?!_)"),
Variable("[a-zA-Z_]+[a-zA-Z0-9_]*");
private final String regex;
}
Каждая строка исходного кода игрушечного языка обрабатывается с помощью этих регулярных выражений; с их помощью мы можем накапливать< /a> список лексем.
Давайте добавим недостающие лексемы, необходимые для нашей модели наследования:
- Во-первых, нам нужно добавить один символ двоеточия
:
, чтобы обозначить наследование класса. Мы можем добавить его в лексемуGroupDivider
с двумя обратными косыми чертами перед двоеточием, чтобы убедиться, что он не будет рассматриваться как специальный символ:
GroupDivider("([|]|,|{|}|.{2}|:)")
- Далее мы должны проверить, есть ли у нас двоеточие среди уже определенных лексем. И действительно, у нас есть лексема
Operator
с двойным двоеточием::
для обозначения доступа к свойству или функции класса. Нам нужно, чтобы это двойное двоеточие::
НЕ рассматривалось как лексемаGroupDivider
с одним двоеточием:
два раза.
Самое надежное решение — поместить отрицательный прогноз (?!:)
после выражения с одним двоеточием, говорящее, что после него не должно быть второго двоеточия:
GroupDivider("([|]|,|{|}|.{2}|(:(?!:)))")
- Для поддержки восходящего и нисходящего приведения нам нужно добавить оператор
as
к лексемеOperator
:
Operator("(+|-|*|/{1,2}|%|>=|>|<=|<{1,2}|={1,2}|!=|!|:{2}s+new|:{2}|(|)|(new|and|or|as)(?=s|$)(?!_))")
- Наконец, мы поместили
is
в качестве оператора проверки типа (экземпляра) в лексемуOperator
:
Operator("(+|-|*|/{1,2}|%|>=|>|<=|<{1,2}|={1,2}|!=|!|:{2}s+new|:{2}|(|)|(new|and|or|as|is)(?=s|$)(?!_))")
Мы добавили все необходимые выражения регулярных выражений в TokenType
. Этими изменениями будет управлять LexicalParser, который преобразует исходный код в токены и обрабатывает их в следующем разделе.
3. Синтаксический анализ
В этом разделе мы преобразуем лексемы, полученные от лексического анализатора, в окончательные утверждения в соответствии с нашими языковыми правилами.
3.1 Объявление класса
В настоящее время мы используем StatementParser< /a> читать и преобразовывать лексемы в определения и утверждения. Чтобы проанализировать определение класса, мы используем метод StatementParser#parseClassDefinition()
.
Все, что мы здесь делаем, — читаем имя класса и его аргументы в квадратных скобках, а в конце строим ClassDefinition:
private void parseClassDefinition() {
// read class definition
Token name = tokens.next(TokenType.Variable);
List<String> properties = new ArrayList<>();
if (tokens.peek(TokenType.GroupDivider, "[")) {
tokens.next(); //skip open square bracket
while (!tokens.peek(TokenType.GroupDivider, "]")) {
Token propertyToken = tokens.next(TokenType.Variable);
properties.add(propertyToken.getValue());
if (tokens.peek(TokenType.GroupDivider, ","))
tokens.next();
}
tokens.next(TokenType.GroupDivider, "]"); //skip close square bracket
}
// read base types
...
// add class definition
...
ClassDefinition classDefinition = new ClassDefinition(name, properties, …);
// parse constructor statements
...
tokens.next(TokenType.Keyword, "end");
}
Для класса Производный мы должны прочитать унаследованные типы и соответствующие ссылочные свойства.
Свойства базового класса и свойства производного класса могут различаться, и для сохранения связи между этими свойствами мы введем вспомогательный класс, который будет содержать имя класса и его свойства:
package org.example.toylanguage.context.definition;
@RequiredArgsConstructor
@Getter
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class ClassDetails {
@EqualsAndHashCode.Include
private final String name;
private final List<String> properties;
}
package org.example.toylanguage.context.definition;
@RequiredArgsConstructor
@Getter
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class ClassDefinition implements Definition {
@EqualsAndHashCode.Include
private final ClassDetails classDetails;
private final Set<ClassDetails> baseTypes;
private final ClassStatement statement;
private final DefinitionScope definitionScope;
}
Теперь давайте заполним унаследованные классы для нашего ClassDefinition
. Мы будем использовать LinkedHashSet
для сохранения порядка Base типов, которые мы будем использовать для инициализации суперконструкторов в порядке, определенном разработчиком:
private void parseClassDefinition() {
// read class definition
...
// read base types
Set<ClassDetails> baseTypes = new LinkedHashSet<>();
if (tokens.peek(TokenType.GroupDivider, ":")) {
while (tokens.peek(TokenType.GroupDivider, ":", ",")) {
tokens.next();
Token baseClassName = tokens.next(TokenType.Variable);
List<String> baseClassProperties = new ArrayList<>();
if (tokens.peek(TokenType.GroupDivider, "[")) {
tokens.next(); //skip open square bracket
while (!tokens.peek(TokenType.GroupDivider, "]")) {
Token baseClassProperty = tokens.next(TokenType.Variable);
baseClassProperties.add(baseClassProperty.getValue());
if (tokens.peek(TokenType.GroupDivider, ","))
tokens.next();
}
tokens.next(TokenType.GroupDivider, "]"); //skip close square bracket
}
ClassDetails baseClassDetails = new ClassDetails(baseClassName.getValue(), baseClassProperties);
baseTypes.add(baseClassDetails);
}
}
// add class definition
...
ClassDetails classDetails = new ClassDetails(name.getValue(), properties);
ClassDefinition classDefinition = new ClassDefinition(classDetails, baseTypes, classStatement, classScope);
// parse constructor statements
...
tokens.next(TokenType.Keyword, "end");
}
3.2 Экземпляр класса
После того, как мы прочитали и сохранили определение класса Derived, мы должны предоставить возможность создать экземпляр с определенными унаследованными типами. В настоящее время для создания экземпляра класса мы используем ClassExpression и ClassValue.
Первый, ClassExpression, используется для создания экземпляра класса в ExpressionReader. Второй, ClassValue, создается с помощью ClassExpression во время выполнения и используется для доступа к свойству класса.
Начнем со второго, ClassValue. Когда мы создаем экземпляр класса и вызываем операторы его конструктора, нам может потребоваться доступ к его свойствам. Каждый производный тип может иметь свой набор свойств, которые не обязательно соответствуют свойствам базового типа.
В качестве следующего требования наших правил наследования, где нам нужно предоставить оператор приведения к типу Base, мы должны создать интерфейс для легкого переключения между типами Base и сохранения переменных. каждого класса Base, изолированного от переменных типа Derived.
Мы определим карту отношений для типов Base и типа Derived для доступа к ClassValue
по имени класса:
@Getter
public class ClassValue extends IterableValue<ClassDefinition> {
private final MemoryScope memoryScope;
private final Map<String, ClassValue> relations;
public ClassValue(ClassDefinition definition, MemoryScope memoryScope, Map<String, ClassValue> relations) {
super(definition);
this.memoryScope = memoryScope;
this.relations = relations;
}
public ClassValue getRelation(String className) {
return relations.get(className);
}
public boolean containsRelation(String className) {
return relations.containsKey(className);
}
...
}
Эти отношения будут использоваться для Upcasting, Downcasting и проверки типа объекта. Чтобы упростить процесс повышения и понижения уровня, эта карта будет содержать одни и те же значения для каждого базового класса в цепочке наследования и позволит выполнять повышающее преобразование из нижнего базового типа в верхний производный тип и наоборот.
Далее давайте изменим ClassExpression, которое мы используем для создания экземпляра класса. Внутри него мы определяем карту отношений во второй раз, которая будет использоваться для создания ClassValue и накапливаться каждым супертипом, который имеет класс Derived:
@RequiredArgsConstructor
@Getter
public class ClassExpression implements Expression {
private final String name;
private final List<? extends Expression> argumentExpressions;
// Base classes and Derived class available to a class instance
private final Map<String, ClassValue> relations;
public ClassExpression(String name, List<Expression> argumentExpressions) {
this(name, argumentExpressions, new HashMap<>());
}
}
Мы также должны обеспечить согласованность для ссылочных свойств. Если мы изменим свойство базового типа, свойство ссылки в производном типе также должно быть обновлено до того же значения, и наоборот, если мы изменим производный тип , необходимо также обновить свойство reference в классе Base:
class A [arg_a]
end
class B [arg_b1, arg_b2]: A [arg_b2]
end
b = new B [ 1, 2 ]
b as A :: arg_a = 3
print b
b :: arg_b2 = 4
print b as A
Output
B [ arg_b1 = 1, arg_b2 = 3 ]
A [ arg_a = 4 ]
Эта ссылочная согласованность может быть делегирована Java путем введения новой оболочки ValueReference для Value, единственный экземпляр которой будет использоваться производным типом для инициализации ссылочного свойства в базовых типах:
package org.example.toylanguage.context;
/**
* Wrapper for the Value to keep the properties relation between a Base class and a Derived class
*
* <pre>{@code
* # Declare the Base class A
* class A [a_value]
* end
*
* # Declare the Derived class B that inherits class A and initializes its `a_value` property with the `b_value` parameter
* class B [b_value]: A [b_value]
* end
*
* # Create an instance of class B
* b = new B [ b_value ]
*
* # If we change the `b_value` property, the A class's property `a_value` should be updated as well
* b :: b_value = new_value
*
* # a_new_value should contain `new_value`
* a_new_value = b as A :: a_value
* }</pre>
*/
@Getter
@Setter
public class ValueReference implements Expression {
private Value<?> value;
private ValueReference(Value<?> value) {
this.value = value;
}
public static ValueReference instanceOf(Expression expression) {
if (expression instanceof ValueReference) {
// reuse variable
return (ValueReference) expression;
} else {
return new ValueReference(expression.evaluate());
}
}
@Override
public Value<?> evaluate() {
return value;
}
}
Теперь давайте инициализируем конструкторы классов Base внутри ClassExpression#evaluate(List<Value<?>>). Этот метод принимает список свойств для создания экземпляров обычных и вложенных классов:
private Value<?> evaluate(List<Value<?>> values) {
//get class's definition and statement
ClassDefinition definition = DefinitionContext.getScope().getClass(name);
ClassStatement classStatement = definition.getStatement();
//set separate scope
MemoryScope classScope = new MemoryScope(null);
MemoryContext.pushScope(classScope);
try {
//initialize constructor arguments
ClassValue classValue = new ClassValue(definition, classScope);
ClassInstanceContext.pushValue(classValue);
IntStream.range(0, definition.getProperties().size()).boxed()
.forEach(i -> MemoryContext.getScope()
.setLocal(definition.getProperties().get(i), values.size() > i ? values.get(i) : NullValue.NULL_INSTANCE));
//execute function body
DefinitionContext.pushScope(definition.getDefinitionScope());
try {
classStatement.execute();
} finally {
DefinitionContext.endScope();
}
return classValue;
} finally {
MemoryContext.endScope();
ClassInstanceContext.popValue();
}
}
Мы изменим часть после того, как установим отдельный MemoryScope
. Во-первых, нам нужно создать экземпляр ClassValue и добавить его на карту отношений:
//set separate scope
MemoryScope classScope = new MemoryScope(null);
MemoryContext.pushScope(classScope);
//initialize constructor arguments
ClassValue classValue = new ClassValue(definition, classScope, relations);
relations.put(name, classValue);
Далее мы преобразуем Value<?> свойства в ValueReference:
List<ValueReference> valueReferences = values.stream()
.map(ValueReference::instanceOf)
.collect(Collectors.toList());
Если мы создадим экземпляр свойства ссылки класса Производный с помощью ValueReference#instanceOf(Expression)
, во второй раз это выражение вернет то же самое ValueReference.
После этого мы можем заполнить недостающие аргументы на случай, если разработчик не предоставит достаточно свойств, определенных в определении класса. Эти отсутствующие свойства можно установить с помощью NullValue:
// fill the missing properties with NullValue.NULL_INSTANCE
// class A [arg1, arg2]
// new A [arg1] -> new A [arg1, null]
// new A [arg1, arg2, arg3] -> new A [arg1, arg2]
List<ValueReference> valuesToSet = IntStream.range(0, definition.getClassDetails().getProperties().size())
.boxed()
.map(i -> values.size() > i ? values.get(i) : ValueReference.instanceOf(NullValue.NULL_INSTANCE))
.collect(Collectors.toList());
Наконец, для этого метода нам нужно создать ClassExpression для каждого класса Base, используя ссылочные свойства класса Derived, а затем выполнить каждый конструктор, вызвав ClassExpression#evaluate. ()код>:
//invoke constructors of the base classes
definition.getBaseTypes()
.stream()
.map(baseType -> {
// initialize base class's properties
// class A [a_arg]
// class B [b_arg1, b_arg2]: A [b_arg1]
List<ValueReference> baseClassProperties = baseType.getProperties().stream()
.map(t -> definition.getClassDetails().getProperties().indexOf(t))
.map(valuesToSet::get)
.collect(Collectors.toList());
return new ClassExpression(baseType.getName(), baseClassProperties, relations);
})
.forEach(ClassExpression::evaluate);
С помощью этого блока кода мы можем создать экземпляр каждого класса Base, который у нас есть в цепочке наследования. Когда мы создаем экземпляр ClassExpression для базового класса, этот базовый класс будет вести себя как производный класс со своими собственными унаследованными базовыми типами, пока мы не достигнем базового класса. который не наследует никакие классы.
После инициализации экземпляров Base мы можем завершить инициализацию экземпляра Derived, задав его свойства с помощью MemoryScope#setLocal(ValueReference)
и выполнив операторы конструктора:
try {
ClassInstanceContext.pushValue(classValue);
IntStream.range(0, definition.getClassDetails().getArguments().size()).boxed()
.forEach(i -> MemoryContext.getScope()
.setLocal(definition.getClassDetails().getArguments().get(i), valuesToSet.get(i)));
//execute constructor statements
DefinitionContext.pushScope(definition.getDefinitionScope());
try {
classStatement.execute();
} finally {
DefinitionContext.endScope();
}
return classValue;
} finally {
MemoryContext.endScope();
ClassInstanceContext.popValue();
}
С новым классом ValueReference в качестве оболочки значений нам также необходимо обновить MemoryScope, чтобы иметь возможность напрямую устанавливать ValueReference и обновлять Value<?> внутри него, если мы изменим свойство класса:
public class MemoryScope {
private final Map<String, ValueReference> variables;
private final MemoryScope parent;
public MemoryScope(MemoryScope parent) {
this.variables = new HashMap<>();
this.parent = parent;
}
public Value<?> get(String name) {
ValueReference variable = variables.get(name);
if (variable != null)
return variable.getValue();
else if (parent != null)
return parent.get(name);
else
return NullValue.NULL_INSTANCE;
}
public Value<?> getLocal(String name) {
ValueReference variable = variables.get(name);
return variable != null ? variable.getValue() : null;
}
public void set(String name, Value<?> value) {
MemoryScope variableScope = findScope(name);
if (variableScope == null) {
setLocal(name, value);
} else {
variableScope.setLocal(name, value);
}
}
// set variable as a reference
public void setLocal(String name, ValueReference variable) {
variables.put(name, variable);
}
// update an existent variable
public void setLocal(String name, Value<?> value) {
if (variables.containsKey(name)) {
variables.get(name).setValue(value);
} else {
variables.put(name, ValueReference.instanceOf(value));
}
}
private MemoryScope findScope(String name) {
if (variables.containsKey(name))
return this;
return parent == null ? null : parent.findScope(name);
}
}
3.3 Функция
В этом подразделе рассматривается вызов функции в модели наследования. В настоящее время для вызова функции мы используем класс FunctionExpression.
Нас интересует только FunctionExpression#evaluate(ClassValue), который принимает ClassValue как тип, который мы используем для выполнения функции из:
/**
* Evaluate class's function
*
* @param classValue instance of class where the function is placed in
*/
public Value<?> evaluate(ClassValue classValue) {
//initialize function arguments
List<Value<?>> values = argumentExpressions.stream().map(Expression::evaluate).collect(Collectors.toList());
//get definition and memory scopes from class definition
ClassDefinition classDefinition = classValue.getValue();
DefinitionScope classDefinitionScope = classDefinition.getDefinitionScope();
MemoryScope memoryScope = classValue.getMemoryScope();
//set class's definition and memory scopes
DefinitionContext.pushScope(classDefinitionScope);
MemoryContext.pushScope(memoryScope);
ClassInstanceContext.pushValue(classValue);
try {
//proceed function
return evaluate(values);
} finally {
DefinitionContext.endScope();
MemoryContext.endScope();
ClassInstanceContext.popValue();
}
}
При наследовании у нас может не быть функции, объявленной в классе Derived. Эта функция может быть доступна только в одном из классов Base. В следующем примере функция action
доступна только в определении класса B.
class A
end
class B
fun action
end
end
class C: A, B
end
c = new C
c :: action []
Чтобы найти класс Base, содержащий функцию с именем и количеством аргументов, мы создадим следующий метод:
private ClassDefinition findClassDefinitionContainingFunction(ClassDefinition classDefinition, String functionName, int argumentsSize) {
DefinitionScope definitionScope = classDefinition.getDefinitionScope();
if (definitionScope.containsFunction(functionName, argumentsSize)) {
return classDefinition;
} else {
for (ClassDetails baseType : classDefinition.getBaseTypes()) {
ClassDefinition baseTypeDefinition = definitionScope.getClass(baseType.getName());
ClassDefinition functionClassDefinition = findClassDefinitionContainingFunction(baseTypeDefinition, functionName, argumentsSize);
if (functionClassDefinition != null)
return functionClassDefinition;
}
return null;
}
}
С помощью этого метода и ранее определенного ClassValue#getRelation(String)
мы можем получить экземпляр ClassValue, который мы можем использовать для вызова функции. Давайте закончим реализацию FunctionExpression#evaluate(ClassValue)
:
/**
* Evaluate class's function
*
* @param classValue instance of class where the function is placed in
*/
public Value<?> evaluate(ClassValue classValue) {
//initialize function arguments
List<Value<?>> values = argumentExpressions.stream().map(Expression::evaluate).collect(Collectors.toList());
// find a class containing the function
ClassDefinition classDefinition = findClassDefinitionForFunction(classValue.getValue(), name, values.size());
if (classDefinition == null) {
throw new ExecutionException(String.format("Function is not defined: %s", name));
}
DefinitionScope classDefinitionScope = classDefinition.getDefinitionScope();
ClassValue functionClassValue = classValue.getRelation(classDefinition.getClassDetails().getName());
MemoryScope memoryScope = functionClassValue.getMemoryScope();
//set class's definition and memory scopes
DefinitionContext.pushScope(classDefinitionScope);
MemoryContext.pushScope(memoryScope);
ClassInstanceContext.pushValue(functionClassValue);
try {
//proceed function
return evaluate(values);
} finally {
DefinitionContext.endScope();
MemoryContext.endScope();
ClassInstanceContext.popValue();
}
}
3.4 Оператор типа приведения
В этом подразделе мы добавим поддержку оператора приведения типа as
. Мы уже определили это выражение в лексеме TokenType.Operator
.
Нам нужно только создать Реализация BinaryOperatorExpression, которая преобразует начальный ClassValue в базовый или производный тип, используя карту ClassValue#relations
:
package org.example.toylanguage.expression.operator;
/**
* Cast a class instance from one type to other
*/
public class ClassCastOperator extends BinaryOperatorExpression {
public ClassCastOperator(Expression left, Expression right) {
super(left, right);
}
@Override
public Value<?> evaluate() {
// evaluate expressions
ClassValue classInstance = (ClassValue) getLeft().evaluate();
String typeToCastName = ((VariableExpression) getRight()).getName();
// retrieve class details
ClassDetails classDetails = classInstance.getValue().getClassDetails();
// check if the type to cast is different from original
if (classDetails.getName().equals(typeToCastName)) {
return classInstance;
} else {
// retrieve ClassValue of other type
return classInstance.getRelation(typeToCastName);
}
}
}
И в качестве последнего шага мы должны подключить этот оператор в Operator с требуемым приоритетом для этой операции:
@RequiredArgsConstructor
@Getter
public enum Operator {
Not("!", NotOperator.class, 7),
ClassInstance("new", ClassInstanceOperator.class, 7),
NestedClassInstance(":{2}s+new", NestedClassInstanceOperator.class, 7),
ClassProperty(":{2}", ClassPropertyOperator.class, 7),
ClassCast("as", ClassCastOperator.class, 7),
...
private final String character;
private final Class<? extends OperatorExpression> type;
private final Integer precedence;
...
}
Это перечисление также построено с использованием модели регулярных выражений и преобразует список лексем в реализации операторов. Предоставленный приоритет будет учитываться с помощью двухстекового алгоритма Дейкстры.
Ознакомьтесь с реализацией ExpressionReader a> и вторая часть для получения дополнительных объяснений.
3.5 Оператор проверки типа
В этом последнем подразделе синтаксического анализа мы определим оператор типа проверки. Реализация будет аналогична оператору приведения, который требует создания OperatorExpression и вставить его в перечисление Operator.
Оператор типа проверки должен возвращать LogicalValue, который обозначает логический тип, содержащий значение true или false:
package org.example.toylanguage.expression.operator;
import org.example.toylanguage.exception.ExecutionException;
import org.example.toylanguage.expression.Expression;
import org.example.toylanguage.expression.VariableExpression;
import org.example.toylanguage.expression.value.ClassValue;
import org.example.toylanguage.expression.value.LogicalValue;
import org.example.toylanguage.expression.value.Value;
public class ClassInstanceOfOperator extends BinaryOperatorExpression {
public ClassInstanceOfOperator(Expression left, Expression right) {
super(left, right);
}
@Override
public Value<?> evaluate() {
Value<?> left = getLeft().evaluate();
// cat = new Cat
// is_cat_animal = cat is Animal
if (left instanceof ClassValue && getRight() instanceof VariableExpression) {
String classType = ((VariableExpression) getRight()).getName();
return new LogicalValue(((ClassValue) left).containsRelation(classType));
} else {
throw new ExecutionException(String.format("Unable to perform `is` operator for the following operands `%s` and `%s`", left, getRight()));
}
}
}
@RequiredArgsConstructor
@Getter
public enum Operator {
Not("!", NotOperator.class, 7),
ClassInstance("new", ClassInstanceOperator.class, 7),
NestedClassInstance(":{2}s+new", NestedClassInstanceOperator.class, 7),
ClassProperty(":{2}", ClassPropertyOperator.class, 7),
ClassCast("as", ClassCastOperator.class, 7),
ClassInstanceOf("is", ClassInstanceOfOperator.class, 7),
...
private final String character;
private final Class<? extends OperatorExpression> type;
private final Integer precedence;
...
}
Вы можете определить свои собственные операторы таким же образом, определив выражение регулярного выражения в лексеме TokenType.Operator
и вставив реализацию OperatorExpression
в Operator
. перечисление.
4. Подведение итогов
Это все изменения, которые нам нужно было сделать для реализации наследования. В этой части мы создали простую гибридную модель наследования как еще один шаг к созданию полноценного языка программирования.
Вот несколько примеров, которые вы можете запустить и протестировать самостоятельно с помощью RunToyLanguage:
class Animal
fun action
print "Animals can run."
end
end
class Bird
fun action
print "Birds can fly."
end
end
class Parrot: Animal, Bird
fun action
this as Bird :: action []
this as Animal :: action []
print "Parrots can talk."
end
end
new Parrot :: action[]
class Add [x, y]
fun sum
return "The sum of " + x + " and " + y + " is " + (x + y)
end
end
class Mul [a, b]
fun mul
return "The multiplication of " + a + " and " + b + " is " + a * b
end
end
class Sub [a, b]
fun sub
return "The subtraction of " + a + " and " + b + " is " + (a - b)
end
end
class Div [m, n]
fun div
return "The division of " + m + " and " + n + " is " + m / n
end
end
class Exp [m, n]
fun exp
return "The exponentiation of " + m + " and " + n + " is " + m ** n
end
end
class Fib [ n ]
fun fib
return "The fibonacci number for " + n + " is " + fib [ n ]
end
fun fib [ n ]
if n < 2
return n
end
return fib [ n - 1 ] + fib [ n - 2 ]
end
end
class Calculator [p, q]: Add [p, q], Sub [q, p],
Mul [p, q], Div [q, p],
Exp [p, q], Fib [ q ]
end
calc = new Calculator [2, 10]
print calc :: sum []
print calc :: sub []
print calc :: mul []
print calc :: div []
print calc :: exp []
print calc :: fib []
Фото Удея Авала на Unsplash
Оригинал