Создание собственного языка программирования с нуля: Часть IX — Гибридное наследование

Создание собственного языка программирования с нуля: Часть IX — Гибридное наследование

25 марта 2023 г.

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

  1. Создание собственного языка программирования с нуля
  2. Построение Ваш собственный язык программирования с нуля: часть II — двухстековый алгоритм Дейкстры
  3. Сборка Ваш собственный язык программирования, часть III: Улучшение лексического анализа с помощью регулярных выражений Lookaheads
  4. Создание собственного языка программирования С нуля, часть 4: реализация функций
  5. Создание собственного языка программирования с нуля : Часть V. Массивы
  6. Создание собственного языка программирования с нуля : Часть VI. Петли
  7. Создание собственного языка программирования с нуля : Часть VII. Классы
  8. Создание собственного языка программирования с нуля: Часть VIII - Вложенные классы

Полный исходный код доступен на GitHub.

1. Модель наследования

Начнем с определения правил наследования для наших классов:

  1. Чтобы наследовать класс, мы будем использовать символ двоеточия :, аналогичный синтаксису C++:
class Base
end

class Derived: Base
end

  1. У нас должна быть возможность заполнить свойства базового класса свойствами производного класса:
class Base [base_arg]
end

class Derived [derived_arg1, derived_arg2]: Base [derived_arg2]
end

  1. Когда мы создаем экземпляр класса 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

  1. Наши классы будут поддерживать гибридное наследование и создавать классы из нескольких типов:
class A
end

class B
end

class C
end

class Derived: A, B, C
end

  1. Чтобы выполнить операцию 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

  1. Чтобы изменить свойства класса 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

  1. Если мы изменим свойство в базовом классе, соответствующее ссылочное свойство в производном классе также должно быть обновлено, и наоборот, если мы изменим < 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 ]

  1. Мы не будем использовать ключевое слово 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

  1. Наконец, в этой части мы добавим оператор 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> список лексем.

Давайте добавим недостающие лексемы, необходимые для нашей модели наследования:

  1. Во-первых, нам нужно добавить один символ двоеточия :, чтобы обозначить наследование класса. Мы можем добавить его в лексему GroupDivider с двумя обратными косыми чертами перед двоеточием, чтобы убедиться, что он не будет рассматриваться как специальный символ:

  1. Далее мы должны проверить, есть ли у нас двоеточие среди уже определенных лексем. И действительно, у нас есть лексема Operator с двойным двоеточием :: для обозначения доступа к свойству или функции класса. Нам нужно, чтобы это двойное двоеточие :: НЕ рассматривалось как лексема GroupDivider с одним двоеточием : два раза.

Самое надежное решение — поместить отрицательный прогноз (?!:) после выражения с одним двоеточием, говорящее, что после него не должно быть второго двоеточия:

  1. Для поддержки восходящего и нисходящего приведения нам нужно добавить оператор as к лексеме Operator:

  1. Наконец, мы поместили is в качестве оператора проверки типа (экземпляра) в лексему Operator:

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

3. Синтаксический анализ

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

3.1 Объявление класса

В настоящее время мы используем StatementParser< /a> читать и преобразовывать лексемы в определения и утверждения. Чтобы проанализировать определение класса, мы используем метод StatementParser#parseClassDefinition().

Все, что мы здесь делаем, — читаем имя класса и его аргументы в квадратных скобках, а в конце строим ClassDefinition:

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

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

Теперь давайте заполним унаследованные классы для нашего ClassDefinition. Мы будем использовать LinkedHashSet для сохранения порядка Base типов, которые мы будем использовать для инициализации суперконструкторов в порядке, определенном разработчиком:

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 и вторая часть для получения дополнительных объяснений.

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


Оригинал