独自のプログラミング言語を作成するこの部分では、ネストされたクラスを実装し、前の部分で紹介したクラスをわずかにアップグレードして、言語を改善し続けます。前の部分をチェックしてください: ゼロから独自のプログラミング言語を構築する ゼロから独自のプログラミング言語を構築する: パート II - ダイクストラの 2 スタック アルゴリズム 独自のプログラミング言語を構築する パート III: 正規表現先読みによる字句解析の改善 ゼロから独自のプログラミング言語を構築する パート IV: 関数の実装 独自のプログラミング言語をゼロから構築する: パート V - 配列 ゼロから独自のプログラミング言語を構築する: パート VI - ループ ゼロから独自のプログラミング言語を構築する: パート VII - クラス 完全なソース コードは で入手できます。 GitHub 1 字句解析 最初のセクションでは、字句解析について説明します。つまり、ソースコードをキーワード、変数、演算子などの言語語彙素に分割するプロセスです。 前の部分から、すべての語彙素タイプを定義するために 列挙型にリストされた正規表現を使用していたことを覚えているかもしれません。 TokenType package org.example.toylanguage.token; public enum TokenType { Comment("\\#.*"), LineBreak("[\\n\\r]"), Whitespace("[\\s\\t]"), 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}|\\(|\\)|(new|and|or)(?=\\s|$))"), Variable("[a-zA-Z_]+[a-zA-Z0-9_]*"); private final String regex; } このネストされたクラスのプロトタイプを調べて、不足している正規表現を 語彙素に追加してみましょう。 TokenType ネストされたクラスのインスタンスを作成するときは、2 つの式とそれらの間に 1 つの演算子を使用して、次の構造を使用します。 左の式 は、ネストされたクラスを取得するために参照しているクラスのインスタンスです。右側の式 は、インスタンスを作成するためのプロパティを持つネストされたクラスです。 class_instance NestedClass [args] 最後に、ネストされたクラスを作成する演算子として、次の式を使用します: 。これは、2 つのコロン 演算子でクラス インスタンス プロパティを参照し、次に でインスタンスを作成することを意味します。オペレーター。 :: new :: new 現在の語彙素のセットでは、 演算子の正規表現を追加するだけです。この演算子は、次の正規表現で検証できます。 :: new :{2}\\s+new クラス プロパティへのアクセスを表す 部分の前に、OR 式として 語彙素にこの式を追加しましょう。 :{2} Operator public enum TokenType { ... Operator("(\\+|-|\\*|/{1,2}|%|>=|>|<=|<{1,2}|={1,2}|!=|!|:{2}\\s+new|:{2}|\\(|\\)|(new|and|or)(?=\\s|$))"), ... } 2 構文解析 2 番目のセクションでは、語彙アナライザーから受け取った語彙素を、言語規則に従って最終ステートメントに変換します。 2.1 演算子式 数式を評価するために、 を使用しています。このアルゴリズムの各演算は、1 つのオペランドを持つ単項演算子、またはそれぞれ 2 つのオペランドを持つ 2 項演算子によって表すことができます。 ダイクストラの Two-Stack アルゴリズム ネストされたクラスのインスタンス化は、左のオペランドが、ネストされたクラスが定義されているクラスを参照するために使用するクラス インスタンスであり、2 番目のオペランドが、インスタンスを作成するネストされたクラスであるバイナリ演算です。 拡張して 実装を作成しましょう NestedClassInstanceOperator : BinaryOperatorExpression package org.example.toylanguage.expression.operator; public class NestedClassInstanceOperator extends BinaryOperatorExpression { public NestedClassInstanceOperator(Expression left, Expression right) { super(left, right); } @Override public Value<?> evaluate() { ... } } 次に、ネストされたクラスのインスタンス化を実行する メソッドを完成させる必要があります。 evaluate() まず、左側のオペランドの式を 式に評価します。 Value @Override public Value<?> evaluate() { // ClassExpression -> ClassValue Value<?> left = getLeft().evaluate(); } 次に、正しいオペランドを する必要があります。この場合、ネストされたクラスの定義が親クラスの DefinitionScope (左側のオペランド) で宣言されているため、 を直接呼び出すことはできません。 evaluate() Expression#evaluate() ネストされたクラスの定義にアクセスするには、左側のオペランドを取り、その DefinitionScope を使用してネストされたクラス定義にアクセスし、次のインスタンスを作成する補助的な メソッドを作成する必要があります。 ClassExpression#evaluate(ClassValue) @Override public Value<?> evaluate() { Value<?> left = getLeft().evaluate(); if (left instanceof ClassValue && getRight() instanceof ClassExpression) { // instantiate nested class // new Class [] :: new NestedClass [] return ((ClassExpression) getRight()).evaluate((ClassValue) left); } else { throw new ExecutionException(String.format("Unable to access class's nested class `%s``", getRight())); } } 最後に、欠落している メソッドを実装しましょう。 ClassExpression#evaluate(ClassValue) この実装は メソッドに似ていますが、唯一の違いは、ネストされたクラス定義を取得するために を設定する必要があることです。 ClassExpression#evaluate() ClassDefinition#getDefinitionScope() package org.example.toylanguage.expression; … public class ClassExpression implements Expression { private final String name; private final List<Expression> argumentExpressions; @Override public Value<?> evaluate() { //initialize class arguments List<Value<?>> values = argumentExpressions.stream().map(Expression::evaluate).collect(Collectors.toList()); return evaluate(values); } /** * Evaluate nested class * * @param classValue instance of the parent class */ public Value<?> evaluate(ClassValue classValue) { //initialize class arguments List<Value<?>> values = argumentExpressions.stream().map(Expression::evaluate).collect(Collectors.toList()); //set parent class's definition ClassDefinition classDefinition = classValue.getValue(); DefinitionContext.pushScope(classDefinition.getDefinitionScope()); try { return evaluate(values); } finally { DefinitionContext.endScope(); } } 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.getArguments().size()).boxed() .forEach(i -> MemoryContext.getScope() .setLocal(definition.getArguments().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(); } } } 2.2 オペレーター 数式を評価するために使用しているすべての演算子は、対応する優先順位、文字、および各演算の結果を計算するために参照する 型と共に 列挙型に格納されます。 OperatorExpression Operator ... public enum Operator { Not("!", NotOperator.class, 7), ClassInstance("new", ClassInstanceOperator.class, 7), ... private final String character; private final Class<? extends OperatorExpression> type; private final Integer precedence; Operator(String character, Class<? extends OperatorExpression> type, Integer precedence) { this.character = character; this.type = type; this.precedence = precedence; } public static Operator getType(String character) { return Arrays.stream(values()) .filter(t -> Objects.equals(t.getCharacter(), character)) .findAny().orElse(null); } ... } 通常のクラスの初期化のための 値はすでにあります。ネストされたクラス インスタンスを管理する新しい値を追加しましょう。 ClassInstance 新しい 値は、以前に で定義したのと同じ文字式を持ち、通常のクラスのインスタンスと同じ優先順位を持ちます。 NestedClassInstance TokenType OperatorExpression タイプについては、以前に定義された を使用します。 NestedClassInstanceOperator ... public enum Operator { Not("!", NotOperator.class, 7), ClassInstance("new", ClassInstanceOperator.class, 7), NestedClassInstance(":{2}\\s+new", NestedClassInstanceOperator.class, 7), ... } この新しい演算子を除いて、文字プロパティに正規表現がないことに気付くかもしれません。正規表現を使用して オペレーターを読み取るには、 メソッドを更新して、オペレーターを正規表現と一致させる必要があります。 NestedClassInstance Operator#getType() public enum Operator { ... public static Operator getType(String character) { return Arrays.stream(values()) .filter(t -> character.matches(t.getCharacter())) .findAny().orElse(null); } ... } 最後に、次の記号を含む操作の文字の前に 2 つのバックスラッシュ を追加する必要があります: これらの文字が正規表現検索記号として扱われないようにします。 \\ +, *, (, ) Multiplication("\\*", MultiplicationOperator.class, 6), Addition("\\+", AdditionOperator.class, 5), LeftParen("\\(", 3), RightParen("\\)", 3), 演算子を導入した後、実際に数式をオペランドと演算子に解析する クラスに挿入する必要があります。クラスインスタンスを読み取る行を見つけるだけです。 NestedClassInstance ExpressionReader if (!operators.isEmpty() && operators.peek() == Operator.ClassInstance) { operand = readClassInstance(token); } オペレーターの読み取りをサポートするために、オペレーターのスタック内の現在のオペレーターに対応する条件を追加します。 NestedClassInstance if (!operators.isEmpty() && (operators.peek() == Operator.ClassInstance || operators.peek() == Operator.NestedClassInstance)) { operand = readClassInstance(token); } メソッドは、通常のクラス宣言を読み取るのと同じ方法で、プロパティを持つネストされたクラスの宣言を読み取ります。このメソッドは、オペランド式全体として インスタンスを返します。 readClassInstance() ClassExpression 3. まとめ すべての準備が整いました。このパートでは、完全なプログラミング言語を作成するためのもう 1 つのステップとして、ネストされたクラスを実装しました。