Trong phần tạo ngôn ngữ lập trình của riêng bạn này, chúng ta sẽ tiếp tục cải thiện ngôn ngữ của mình bằng cách triển khai các lớp lồng nhau và nâng cấp một chút các lớp được giới thiệu trong phần trước. Mời các bạn xem lại các phần trước:
Mã nguồn đầy đủ có sẵn trên GitHub .
Trong phần đầu tiên, chúng ta sẽ đề cập đến phân tích từ vựng. Tóm lại, đó là một quá trình phân chia mã nguồn thành các từ vựng ngôn ngữ, chẳng hạn như từ khóa, biến, toán tử, v.v.
Bạn có thể nhớ từ các phần trước rằng tôi đã sử dụng các biểu thức regex được liệt kê trong TokenType enum để xác định tất cả các loại từ vựng:
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; }
Hãy xem xét nguyên mẫu lớp lồng nhau này và thêm các biểu thức regex còn thiếu vào các từ TokenType
của chúng tôi:
Khi chúng ta tạo một thể hiện của một lớp lồng nhau, chúng ta sử dụng cấu trúc sau đây với hai biểu thức và một toán tử giữa chúng:
Biểu thức bên trái class_instance
là một thể hiện của lớp mà chúng ta đang đề cập đến để lấy một lớp lồng nhau từ đó. Biểu thức bên phải NestedClass [args]
là một lớp lồng nhau với các thuộc tính để tạo một thể hiện.
Cuối cùng, với tư cách là một toán tử để tạo một lớp lồng nhau, tôi sẽ sử dụng biểu thức sau: :: new
, nghĩa là chúng ta tham chiếu đến thuộc tính thể hiện của lớp bằng hai toán tử dấu hai chấm ::
và sau đó chúng ta tạo một thể hiện với biểu thức new
nhà điều hành.
Với tập hợp các từ vựng hiện tại, chúng ta chỉ cần thêm một biểu thức chính quy cho toán tử :: new
. Toán tử này có thể được xác thực bằng biểu thức chính quy sau:
:{2}\\s+new
Hãy thêm biểu thức này vào từ vựng Operator
dưới dạng biểu thức OR trước phần :{2}
đại diện cho việc truy cập thuộc tính lớp:
public enum TokenType { ... Operator("(\\+|-|\\*|/{1,2}|%|>=|>|<=|<{1,2}|={1,2}|!=|!|:{2}\\s+new|:{2}|\\(|\\)|(new|and|or)(?=\\s|$))"), ... }
Trong phần thứ hai, chúng tôi sẽ chuyển đổi các từ vựng nhận được từ bộ phân tích từ vựng thành các câu lệnh cuối cùng tuân theo các quy tắc ngôn ngữ của chúng tôi.
Để đánh giá các biểu thức toán học, chúng tôi đang sử dụng thuật toán Dijkstra's Two-Stack . Mỗi phép toán trong thuật toán này có thể được trình bày bởi toán tử một ngôi với một toán hạng hoặc bởi toán tử nhị phân với hai toán hạng tương ứng:
Khởi tạo của lớp lồng nhau là một phép toán nhị phân trong đó toán hạng bên trái là một thể hiện của lớp mà chúng ta sử dụng để chỉ lớp mà lớp lồng nhau được xác định và toán hạng thứ hai là một lớp lồng nhau mà chúng ta tạo một thể hiện của:
Hãy tạo triển NestedClassInstanceOperator
bằng cách mở rộng
package org.example.toylanguage.expression.operator; public class NestedClassInstanceOperator extends BinaryOperatorExpression { public NestedClassInstanceOperator(Expression left, Expression right) { super(left, right); } @Override public Value<?> evaluate() { ... } }
Tiếp theo, chúng ta nên hoàn thành phương thức evaluate()
sẽ thực hiện khởi tạo lớp lồng nhau:
Đầu tiên, chúng tôi đánh giá biểu thức của toán hạng bên trái thành biểu thức Value
:
@Override public Value<?> evaluate() { // ClassExpression -> ClassValue Value<?> left = getLeft().evaluate(); }
Tiếp theo, chúng ta cần evaluate()
toán hạng phù hợp. Trong trường hợp này, chúng ta không thể gọi trực tiếp Expression#evaluate()
vì các định nghĩa của các lớp lồng nhau được khai báo trong Phạm vi định nghĩa của lớp cha (trong toán hạng bên trái).
Để truy cập các định nghĩa của các lớp lồng nhau, chúng ta nên tạo một phương thức phụ trợ ClassExpression#evaluate(ClassValue)
sẽ lấy toán hạng bên trái và sử dụng DefinitionScope của nó để truy cập định nghĩa lớp lồng nhau và tạo một thể hiện của:
@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())); } }
Cuối cùng, hãy triển khai phương ClassExpression#evaluate(ClassValue)
bị thiếu.
Việc triển khai này sẽ tương tự như phương ClassExpression#evaluate()
với sự khác biệt duy nhất là chúng ta nên đặt ClassDefinition#getDefinitionScope()
để truy xuất các định nghĩa lớp lồng nhau:
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(); } } }
Tất cả các toán tử chúng ta đang sử dụng để đánh giá các biểu thức toán học được lưu trữ trong Operator enum với thứ tự ưu tiên, ký tự và loại OperatorExpression
tương ứng mà chúng ta tham khảo để tính toán kết quả của từng thao tác:
... 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); } ... }
Chúng ta đã có giá trị ClassInstance
để khởi tạo một lớp thông thường. Hãy thêm một giá trị mới để quản lý các thể hiện của lớp lồng nhau.
Giá trị NestedClassInstance
mới sẽ có biểu thức ký tự giống như chúng ta đã xác định trong TokenType trước đó và cùng mức độ ưu tiên như thể hiện của lớp thông thường.
Đối với loại OperatorExpression, chúng tôi sẽ sử dụng NestedClassInstanceOperator
đã xác định trước đó:
... public enum Operator { Not("!", NotOperator.class, 7), ClassInstance("new", ClassInstanceOperator.class, 7), NestedClassInstance(":{2}\\s+new", NestedClassInstanceOperator.class, 7), ... }
Bạn có thể nhận thấy rằng chúng tôi không có biểu thức chính quy trong thuộc tính ký tự ngoại trừ toán tử mới này. Để đọc toán tử NestedClassInstance
bằng cách sử dụng biểu thức chính quy, chúng ta nên cập nhật phương thức Operator#getType()
để khớp toán tử với biểu thức chính quy:
public enum Operator { ... public static Operator getType(String character) { return Arrays.stream(values()) .filter(t -> character.matches(t.getCharacter())) .findAny().orElse(null); } ... }
Cuối cùng, chúng ta nên thêm hai dấu gạch chéo ngược \\
trước một ký tự cho các thao tác chứa các ký hiệu sau: +, *, (, )
để đảm bảo các ký tự này không được coi là ký hiệu tìm kiếm regex:
Multiplication("\\*", MultiplicationOperator.class, 6), Addition("\\+", AdditionOperator.class, 5), LeftParen("\\(", 3), RightParen("\\)", 3),
Sau khi chúng tôi giới thiệu toán tử NestedClassInstance
, chúng tôi nên thêm toán tử này vào lớp ExpressionReader để thực sự phân tích các biểu thức toán học thành các toán hạng và toán tử. Chúng ta chỉ cần tìm dòng nơi chúng ta đọc thể hiện của lớp:
if (!operators.isEmpty() && operators.peek() == Operator.ClassInstance) { operand = readClassInstance(token); }
Để hỗ trợ đọc toán tử NestedClassInstance
, chúng tôi thêm điều kiện tương ứng cho toán tử hiện tại trong ngăn xếp của toán tử:
if (!operators.isEmpty() && (operators.peek() == Operator.ClassInstance || operators.peek() == Operator.NestedClassInstance)) { operand = readClassInstance(token); }
Phương readClassInstance()
sẽ đọc khai báo của lớp lồng nhau với các thuộc tính giống như cách nó đọc khai báo lớp thông thường. Phương thức này trả về thể ClassExpression
dưới dạng toàn bộ biểu thức toán hạng.
Mọi thứ đã sẵn sàng ngay bây giờ. Trong phần này, chúng tôi đã triển khai các lớp lồng nhau như một bước nữa để tạo ra một ngôn ngữ lập trình hoàn chỉnh.