paint-brush
ゼロから独自のプログラミング言語を構築する: パート VII - クラス@alexandermakeev
6,103 測定値
6,103 測定値

ゼロから独自のプログラミング言語を構築する: パート VII - クラス

Alexander Makeev22m2022/12/15
Read on Terminal Reader

長すぎる; 読むには

独自のプログラミング言語を作成するこの部分では、クラスを実装し、最後に実際の Stack 実装を記述します。
featured image - ゼロから独自のプログラミング言語を構築する: パート VII - クラス
Alexander Makeev HackerNoon profile picture

独自のプログラミング言語を作成するこの部分では、以前に定義された構造の拡張としてクラスを実装します。前の部分をチェックしてください:


  1. ゼロから独自のプログラミング言語を構築する
  2. ゼロから独自のプログラミング言語を構築する: パート II - ダイクストラの 2 スタック アルゴリズム
  3. 独自のプログラミング言語を構築する パート III: 正規表現先読みによる字句解析の改善
  4. ゼロから独自のプログラミング言語を構築する パート IV: 関数の実装
  5. 独自のプログラミング言語をゼロから構築する: パート V - 配列
  6. ゼロから独自のプログラミング言語を構築する: パート VI - ループ

完全なソースコードが利用可能ですGitHubで.


1.字句解析

最初のセクションでは、字句解析について説明します。つまり、ソースコードをキーワード、変数、演算子などの言語語彙素に分割するプロセスです。


前の部分で、すべての語彙素タイプを定義するためにTokenType列挙型の正規表現を使用していたことを覚えているかもしれません。


次のクラス プロトタイプを見て、不足している正規表現部分をTokenType語彙素に追加してみましょう。


  1. まず、クラス宣言がどこから始まるかを語彙アナライザーに知らせるために、 class単語をKeyword語彙素の式に入れる必要があります。
 package org.example.toylanguage.token; ... public enum TokenType { ... Keyword("(if|elif|else|end|print|input|fun|return|loop|in|by|break|next|class)(?=\\s|$)"), ... private final String regex; }


  1. 次に、現在のオブジェクトへの参照のマーカーとして、新しいThis語彙素が必要です。
 public enum TokenType { ... This("(this)(?=,|\\s|$)"); private final String regex; }


2.構文解析

2 番目のセクションでは、語彙アナライザーから受け取った語彙素を、言語規則に従って最終ステートメントに変換します。

2.1 定義範囲

クラスまたは関数を宣言するとき、この宣言は定義された分離境界内で使用できる必要があります。たとえば、次のリストでturn_on []という名前の関数を宣言すると、宣言後に実行できるようになります。


しかし、クラス スコープ内で同じ関数を宣言すると、この関数はメイン ブロックから直接アクセスできなくなります。


  1. これらの定義境界を実装するには、 DefinitionScopeクラスを作成し、宣言されたすべての定義をクラスと関数の 2 つのセット内に格納します。
 package org.example.toylanguage.context.definition; public class DefinitionScope { private final Set<ClassDefinition> classes; private final Set<FunctionDefinition> functions; public DefinitionScope() { this.classes = new HashSet<>(); this.functions = new HashSet<>(); } }


  1. さらに、親の定義スコープにアクセスしたい場合があります。たとえば、2 つの別個のクラスを宣言し、2 番目のクラス内に最初のクラスのインスタンスを作成するとします。


この機能を提供するために、 DefinitionScope親インスタンスを上位層への参照として追加します。これを使用して、最上位の定義層に到達します。

 public class DefinitionScope { private final Set<ClassDefinition> classes; private final Set<FunctionDefinition> functions; private final DefinitionScope parent; public DefinitionScope(DefinitionScope parent) { this.classes = new HashSet<>(); this.functions = new HashSet<>(); this.parent = parent; } }


  1. それでは、定義を追加し、親スコープを使用して名前で取得するためのインターフェイスを提供して、実装を完了しましょう。
 public class DefinitionScope { … public ClassDefinition getClass(String name) { Optional<ClassDefinition> classDefinition = classes.stream() .filter(t -> t.getName().equals(name)) .findAny(); if (classDefinition.isPresent()) return classDefinition.get(); else if (parent != null) return parent.getClass(name); else throw new ExecutionException(String.format("Class is not defined: %s", name)); } public void addClass(ClassDefinition classDefinition) { classes.add(classDefinition); } public FunctionDefinition getFunction(String name) { Optional<FunctionDefinition> functionDefinition = functions.stream() .filter(t -> t.getName().equals(name)) .findAny(); if (functionDefinition.isPresent()) return functionDefinition.get(); else if (parent != null) return parent.getFunction(name); else throw new ExecutionException(String.format("Function is not defined: %s", name)); } public void addFunction(FunctionDefinition functionDefinition) { functions.add(functionDefinition); } }


  1. 最後に、宣言された定義スコープを管理し、それらを切り替えるために、 java.util.Stackコレクション (LIFO) を使用してコンテキスト クラスを作成します。
 package org.example.toylanguage.context.definition; public class DefinitionContext { private final static Stack<DefinitionScope> scopes = new Stack<>(); public static DefinitionScope getScope() { return scopes.peek(); } public static DefinitionScope newScope() { return new DefinitionScope(scopes.isEmpty() ? null : scopes.peek()); } public static void pushScope(DefinitionScope scope) { scopes.push(scope); } public static void endScope() { scopes.pop(); } }


2.2 メモリ範囲

このセクションでは、クラス変数と関数変数を管理するためのMemoryScopeします。


  1. 宣言された各変数は、クラスまたは関数の定義と同様に、分離されたコード ブロック内でのみアクセスできる必要があります。たとえば、次のリストで変数を定義すると、宣言の直後にアクセスできます。


しかし、関数またはクラス内で変数を宣言すると、変数はコードのメイン (上部) ブロックから使用できなくなります。


このロジックを実装し、特定のスコープで定義された変数を格納するには、変数名をキーとして、変数Valueを値として持つマップを含むMemoryScopeクラスを作成します。

 public class MemoryScope { private final Map<String, Value<?>> variables; public MemoryScope() { this.variables = new HashMap<>(); } }


  1. 次に、 DefinitionScopeと同様に、親のスコープ変数へのアクセスを提供します。
 public class MemoryScope { private final Map<String, Value<?>> variables; private final MemoryScope parent; public MemoryScope(MemoryScope parent) { this.variables = new HashMap<>(); this.parent = parent; } }


  1. 次に、変数を取得および設定するメソッドを追加します。変数を設定するとき、上位のMemoryScopeレイヤーに既に定義済みの変数がある場合は、常に以前に設定した値を再割り当てします。
 public class MemoryScope { ... public Value<?> get(String name) { Value<?> value = variables.get(name); if (value != null) return value; else if (parent != null) return parent.get(name); else return NullValue.NULL_INSTANCE; } public void set(String name, Value<?> value) { MemoryScope variableScope = findScope(name); if (variableScope == null) { variables.put(name, value); } else { variableScope.variables.put(name, value); } } private MemoryScope findScope(String name) { if (variables.containsKey(name)) return this; return parent == null ? null : parent.findScope(name); } }


  1. setメソッドとgetメソッドに加えて、 MemoryScopeの現在の (ローカル) レイヤーとやり取りするための 2 つの実装を追加します。
 public class MemoryScope { ... public Value<?> getLocal(String name) { return variables.get(name); } public void setLocal(String name, Value<?> value) { variables.put(name, value); } }


これらのメソッドは、後で関数の引数またはクラスのインスタンス引数を初期化するために使用されます。たとえば、 Lampクラスのインスタンスを作成し、定義済みのグローバルtype変数を渡す場合、 lamp_instance :: typeプロパティを更新しようとするときに、この変数を変更しないでください。


  1. 最後に、変数を管理し、メモリ スコープを切り替えるために、 java.util.Stackコレクションを使用してMemoryContext実装を作成します。
 package org.example.toylanguage.context; public class MemoryContext { private static final Stack<MemoryScope> scopes = new Stack<>(); public static MemoryScope getScope() { return scopes.peek(); } public static MemoryScope newScope() { return new MemoryScope(scopes.isEmpty() ? null : scopes.peek()); } public static void pushScope(MemoryScope scope) { scopes.push(scope); } public static void endScope() { scopes.pop(); } }


2.3 クラス定義

このセクションでは、クラス定義を読み取って保存します。


  1. まず、 Statementの実装を作成します。このステートメントは、クラスのインスタンスを作成するたびに実行されます。
 package org.example.toylanguage.statement; public class ClassStatement { }


  1. 各クラスには、コンストラクターなど、初期化およびその他の操作用のネストされたステートメントを含めることができます。これらのステートメントを保存するために、実行するネストされたステートメントのリストを含むCompositeStatementを拡張します。
 public class ClassStatement extends CompositeStatement { }


  1. 次に、 ClassDefinitionを宣言して、クラス名、その引数、コンストラクター ステートメント、およびクラスの関数を含む定義スコープを格納します。
 package org.example.toylanguage.context.definition; import java.util.List; @RequiredArgsConstructor @Getter @EqualsAndHashCode(onlyExplicitlyIncluded = true) public class ClassDefinition implements Definition { @EqualsAndHashCode.Include private final String name; private final List<String> arguments; private final ClassStatement statement; private final DefinitionScope definitionScope; }


  1. これで、 StatementParserを使用してクラス宣言を読み取る準備が整いました。 class値を持つ語彙素のキーワードに出会ったら、まずクラス名と角括弧内の引数を読み取る必要があります。


 package org.example.toylanguage; public class StatementParser { ... private void parseClassDefinition() { Token type = tokens.next(TokenType.Variable); List<String> arguments = new ArrayList<>(); if (tokens.peek(TokenType.GroupDivider, "[")) { tokens.next(TokenType.GroupDivider, "["); //skip opening square bracket while (!tokens.peek(TokenType.GroupDivider, "]")) { Token argumentToken = tokens.next(TokenType.Variable); arguments.add(argumentToken.getValue()); if (tokens.peek(TokenType.GroupDivider, ",")) tokens.next(); } tokens.next(TokenType.GroupDivider, "]"); //skip closing square bracket } } }


  1. 引数の後に、ネストされたコンストラクター ステートメントを読み取る必要があります。


これらのステートメントを保存するために、以前に定義したClassStatementのインスタンスを作成します。

 private void parseClassDefinition() { ... ClassStatement classStatement = new ClassStatement(); }


  1. 引数とネストされたステートメントに加えて、クラスには関数を含めることもできます。これらの関数にクラス定義内でのみアクセスできるようにするために、 DefinitionScopeの新しいレイヤーを初期化します。
 private void parseClassDefinition() { ... ClassStatement classStatement = new ClassStatement(); DefinitionScope classScope = DefinitionContext.newScope(); }


  1. 次に、 ClassDefinitionのインスタンスを初期化し、それをDefinitionContextに配置します。
 private void parseClassDefinition() { ... ClassStatement classStatement = new ClassStatement(); DefinitionScope classScope = DefinitionContext.newScope(); ClassDefinition classDefinition = new ClassDefinition(type.getValue(), arguments, classStatement, classScope); DefinitionContext.getScope().addClass(classDefinition); }


  1. 最後に、クラス コンストラクターのステートメントと関数を読み取るために、静的なStatementParser#parse()メソッドを呼び出します。このメソッドは、最後にスキップする必要があるend語彙素に到達するまで、 classStatementインスタンス内のステートメントを収集します。
 private void parseClassDefinition() { ... //parse class statements StatementParser.parse(this, classStatement, classScope); tokens.next(TokenType.Keyword, "end"); }


2.4 クラスインスタンス

この時点で、コンストラクター ステートメントと関数を含むクラス定義を既に読み取ることができます。それでは、クラスのインスタンスを解析しましょう。


  1. まず、各クラス インスタンスの状態を含むClassValueを定義します。クラスは、関数とは異なり、永続的なMemoryScopeを持つ必要があり、このスコープは、クラス インスタンスと対話するたびに、すべてのクラスのインスタンス引数と状態変数で使用できる必要があります。
 public class ClassValue extends IterableValue<ClassDefinition> { private final MemoryScope memoryScope; public ClassValue(ClassDefinition definition, MemoryScope memoryScope) { super(definition); this.memoryScope = memoryScope; } }


  1. 次に、 MemoryContextを使用してクラスのインスタンス プロパティを操作するメソッドを提供します。
 public class ClassValue extends IterableValue<ClassDefinition> { private final MemoryScope memoryScope; public ClassValue(ClassDefinition definition, MemoryScope memoryScope) { super(definition); this.memoryScope = memoryScope; } @Override public String toString() { return getValue().getArguments().stream() .map(t -> t + " = " + getValue(t)) .collect(Collectors.joining(", ", getValue().getName() + " [ ", " ]")); } public Value<?> getValue(String name) { Value<?> result = MemoryContext.getScope().getLocal(name); return result != null ? result : NULL_INSTANCE; } public void setValue(String name, Value<?> value) { MemoryContext.getScope().setLocal(name, value); } }


  1. MemoryScope#getLocal()およびMemoryScope#setLocal()メソッドを呼び出すことで、 MemoryScope変数の現在のレイヤーを操作することに注意してください。ただし、クラスのインスタンス状態にアクセスする前に、そのMemoryScopeMemoryContextに配置し、終了したら解放する必要があります。
 public class ClassValue extends IterableValue<ClassDefinition> { ... public Value<?> getValue(String name) { MemoryContext.pushScope(memoryScope); try { Value<?> result = MemoryContext.getScope().getLocal(name); return result != null ? result : NULL_INSTANCE; } finally { MemoryContext.endScope(); } } public void setValue(String name, Value<?> value) { MemoryContext.pushScope(memoryScope); try { MemoryContext.getScope().setLocal(name, value); } finally { MemoryContext.endScope(); } } }


  1. 次に、構文解析中に定義済みのクラス インスタンスを構築するために使用される残りのClassExpressionを実装できます。クラスのインスタンス定義を宣言するために、ステートメントの実行中に最終的なValueインスタンスに変換されるClassDefinitionExpression引数のリストを提供します。
 package org.example.toylanguage.expression; @RequiredArgsConstructor @Getter public class ClassExpression implements Expression { private final ClassDefinition definition; private final List<Expression> arguments; @Override public Value<?> evaluate() { ... } }


  1. 以前に定義されたClassValueのインスタンスを作成するために実行中に使用されるExpression#evaluate()メソッドを実装しましょう。まず、 Expression引数をValue引数に評価します。
 @Override public Value<?> evaluate() { //initialize class arguments List<Value<?>> values = arguments.stream() .map(Expression::evaluate) .collect(Collectors.toList()); }


  1. 次に、他の変数から分離する必要があり、クラスのインスタンス状態変数のみを含めることができる空のメモリ スコープを作成します。
 @Override public Value<?> evaluate() { //initialize class arguments List<Value<?>> values = arguments.stream().map(Expression::evaluate).collect(Collectors.toList()); //get class's definition and statement ClassStatement classStatement = definition.getStatement(); //set separate scope MemoryScope classScope = new MemoryScope(null); MemoryContext.pushScope(classScope); try { ... } finally { MemoryContext.endScope(); } }


  1. 次に、 ClassValueのインスタンスを作成し、クラスのValue引数を分離メモリ スコープに書き込みます。
 try { //initialize constructor arguments ClassValue classValue = new ClassValue(definition, classScope); 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)); ... } finally { MemoryContext.endScope(); }


空のMemoryScopeを設定する前に、 Expression引数をValue引数に変換したことに注意してください。そうしないと、クラスのインスタンス引数にアクセスできなくなります。次に例を示します。


  1. 最後に、 ClassStatementを実行できます。ただし、その前に、クラスのDefinitionScopeを設定して、コンストラクター ステートメント内でクラスの関数にアクセスできるようにする必要があります。
 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(); }


9*.最後に、クラスをより柔軟にし、ユーザーがクラス定義を宣言する前にクラス インスタンスを作成できるようにします。


これは、 ClassDefinitionの初期化をDefinitionContextに委任し、式を評価するときにのみアクセスすることで実行できます。

 public class ClassExpression implements Expression { private final String name; private final List<Expression> arguments; @Override public Value<?> evaluate() { //get class's definition and statement ClassDefinition definition = DefinitionContext.getScope().getClass(name); ... } }


FunctionExpression に対して同じ委任を行って、定義の前に関数を呼び出すことができます。


  1. 最後に、 ExpressionReaderを使用してクラス インスタンスの読み取りを終了できます。 以前に定義された構造インスタンス間に違いはありません。 Expression引数を読み取り、 ClassExpressionを構築するだけです。
 public class ExpressionReader ... // read class instance: new Class[arguments] private ClassExpression readClassInstance(Token token) { List<Expression> arguments = new ArrayList<>(); if (tokens.peekSameLine(TokenType.GroupDivider, "[")) { tokens.next(TokenType.GroupDivider, "["); //skip opening square bracket while (!tokens.peekSameLine(TokenType.GroupDivider, "]")) { Expression value = ExpressionReader.readExpression(this); arguments.add(value); if (tokens.peekSameLine(TokenType.GroupDivider, ",")) tokens.next(); } tokens.next(TokenType.GroupDivider, "]"); //skip closing square bracket } return new ClassExpression(token.getValue(), arguments); } }


2.5 クラス機能

この時点で、クラスを作成し、クラスのコンストラクター ステートメントを実行できます。しかし、まだクラスの機能を実行できません。

  1. 関数を呼び出したいクラス インスタンスへの参照としてClassValueを受け入れるFunctionExpression#evaluateメソッドをオーバーロードしましょう。
 package org.example.toylanguage.expression; public class FunctionExpression implements Expression { ... public Value<?> evaluate(ClassValue classValue) { } }


  1. 次のステップは、現在のMemoryScopeを使用して、関数Expression引数をValue引数に変換することです。
 public Value<?> evaluate(ClassValue classValue) { //initialize function arguments List<Value<?>> values = argumentExpressions.stream() .map(Expression::evaluate) .collect(Collectors.toList()); }


  1. 次に、クラスのMemoryScopeDefinitionScopeをコンテキストに渡す必要があります。
 ... //get definition and memory scopes from class definition ClassDefinition classDefinition = classValue.getValue(); DefinitionScope classDefinitionScope = classDefinition.getDefinitionScope(); //set class's definition and memory scopes DefinitionContext.pushScope(classDefinitionScope); MemoryContext.pushScope(memoryScope);


  1. 最後に、この実装では、デフォルトのFunctionExpression#evaluate(List<Value<?>> values)メソッドを呼び出し、評価されたValue引数を渡します。
 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); try { //proceed function return evaluate(values); } finally { DefinitionContext.endScope(); MemoryContext.endScope(); } }


  1. クラスの関数を呼び出すには、二重コロン::演算子を使用します。現在、この演算子は、クラスのプロパティへのアクセスを担当するClassPropertyOperator ( StructureValueOperator ) 実装によって管理されています。


同じ二重コロン::文字を使用した関数呼び出しをサポートするように改善しましょう:


クラスの関数は、左の式がClassExpressionで、2 番目の式がFunctionExpressionである場合にのみ、この演算子で管理できます。

 package org.example.toylanguage.expression.operator; public class ClassPropertyOperator extends BinaryOperatorExpression implements AssignExpression { public ClassPropertyOperator(Expression left, Expression right) { super(left, right); } @Override public Value<?> evaluate() { Value<?> left = getLeft().evaluate(); if (left instanceof ClassValue) { if (getRight() instanceof VariableExpression) { // access class's property // new ClassInstance[] :: class_argument return ((ClassValue) left).getValue(((VariableExpression) getRight()).getName()); } else if (getRight() instanceof FunctionExpression) { // execute class's function // new ClassInstance[] :: class_function [] return ((FunctionExpression) getRight()).evaluate((ClassValue) left); } } throw new ExecutionException(String.format("Unable to access class's property `%s``", getRight())); } @Override public void assign(Value<?> value) { Value<?> left = getLeft().evaluate(); if (left instanceof ClassValue && getRight() instanceof VariableExpression) { String propertyName = ((VariableExpression) getRight()).getName(); ((ClassValue) left).setValue(propertyName, value); } } }


3. まとめ

この部分では、クラスを実装しました。スタック実装など、より複雑なものを作成できるようになりました。

 main [] fun main [] stack = new Stack [] loop num in 0..5 # push 0,1,2,3,4 stack :: push [ num ] end size = stack :: size [] loop i in 0..size # should print 4,3,2,1,0 print stack :: pop [] end end class Stack [] arr = {} n = 0 fun push [ item ] this :: arr << item n = n + 1 end fun pop [] n = n - 1 item = arr { n } arr { n } = null return item end fun size [] return this :: n end end