paint-brush
Xây dựng ngôn ngữ lập trình của riêng bạn từ đầu: Phần VIII - Các lớp lồng nhautừ tác giả@alexandermakeev
676 lượt đọc
676 lượt đọc

Xây dựng ngôn ngữ lập trình của riêng bạn từ đầu: Phần VIII - Các lớp lồng nhau

từ tác giả Alexander Makeev9m2023/01/16
Read on Terminal Reader

dài quá đọc không nổi

Trong phần tạo ngôn ngữ lập trình của riêng bạn này, chúng ta sẽ triển khai các lớp lồng nhau
featured image - Xây dựng ngôn ngữ lập trình của riêng bạn từ đầu: Phần VIII - Các lớp lồng nhau
Alexander Makeev HackerNoon profile picture

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:


  1. Xây dựng ngôn ngữ lập trình của riêng bạn từ đầu
  2. Xây dựng ngôn ngữ lập trình của riêng bạn từ đầu: Phần II - Thuật toán hai ngăn xếp của Dijkstra
  3. Xây dựng ngôn ngữ lập trình của riêng bạn Phần III: Cải thiện phân tích từ vựng với Regex Lookaheads
  4. Xây dựng ngôn ngữ lập trình của riêng bạn từ đầu Phần IV: Thực hiện các chức năng
  5. Xây dựng ngôn ngữ lập trình của riêng bạn từ đầu: Phần V - Mảng
  6. Xây dựng ngôn ngữ lập trình của riêng bạn từ đầu: Phần VI - Vòng lặp
  7. Xây dựng ngôn ngữ lập trình của riêng bạn từ đầu: Phần VII - Các lớp học


Mã nguồn đầy đủ có sẵn trên GitHub .

1 Phân tích từ vựng

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|$))"), ... }


2 Phân tích cú pháp

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.

2.1 Biểu thức toán tử

Để đá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

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() { ... } }


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(); } } }


2.2 Toán tử

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.

3. Kết thúc

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.