Định nghĩa token và các phương thức làm việc trên token

Một phần của tài liệu Mô Tả Ngôn Ngữ Lập Trình Và Viết Chương Trình Xây Dựng Cây Phân Tích Cú Pháp (Trang 28)

II. Xây dựng trình phân tích cú pháp trong C#

1. Định nghĩa token và các phương thức làm việc trên token

Trong C# chúng ta có thể khai báo một luồng token bằng đối tượng Ienumerator<Token>. Phương thức ts.Current để lấy ra một token hiện hành, và phương thức ts.MoveNext() đọc token tiếp theo trong luồng. Để báo hiệu đầu vào có lỗi, chương trình sẽ ném một ngoại lệ có kiểu là ApplicationExeption. Trình phân tích cú pháp phải truy cập đầu vào thông qua chỉ đổi tượng ts (token stream). Kiểu Token phải có một thuộc tính có kiểu enum tên là Kind để đặc tả kiểu của nó.

Một token đơn giản, tương ứng với một ký hiệu terminal như "-" có thể được mô ta bởi một token có loại enum được thiết lập là một giá trị SUB. Vì vậy, chúng ta sẽ mô tả cho các token với: một khai báo enum, Kind, và khai báo một cấu trúc, Token

public enum Kind { EOF, PLUS, MINUS, MULT, DIV, LPAR, RPAR, NUM } public class Token

{

public readonly Kind kind; public readonly double nval;

private Token(Kind k) { kind = k; nval = 0; }

private Token(double n) { kind = Kind.NUM; nval = n; } public override string ToString()

{

if (kind == Kind.NUM) return "NUM(" + nval + ")"; else return kind.ToString();

}

public static Token FromKind(Kind k) { return new Token(k); } public static Token FromDouble(double d) { return new Token(d); } }

Để sử dụng tên TokenStream, thay cho System.Collection.Generic.IEnumerator chúng ta sẽ sử dụng sau đây bằng cách sử dụng khai báo như sau:

using TokenStream = System.Collections.Generic.IEnumerator <Token>; 2. Xây dựng trình phân tích cú pháp (parser) trong C #

Một trình phân tích cú pháp cho một ngữ pháp G có thể được xây dựng một các hệ thống nhờ các quy tắc ngữ pháp. Trình phân tích cú pháp sẽ bao gồm một tập hợp các phương pháp phân tích cú pháp đệ quy, cho mỗi ký hiệu nonterminal trong ngữ pháp.

Trang 25

Phương pháp phân tích cú pháp tương ứng với một ký hiệu nonterminal A cũng được gọi là A. Nó cố gắng để tìm một chuỗi có thể dẫn xuất từ A tại token hiện hành. Nếu thành công, sau đó nó trả về, và sau khi đọc nhiều token hơn từ luồng token (token stream). Nếu nó không thành công, sau đó nó ném một ngoại lệ của lớp ApplicationException. Trình phân tích cú pháp G = (T,N,R,S) có dạng void A1 (TokenStream ts) { ... } void A2 (TokenStream ts) { ... } ... void Ak (TokenStream ts) { ... } void Parse(TokenStream ts) { ts.MoveNext(); S(ts); if (ts.Current.kind != Kind.EOF)

throw new ApplicationException("Expected end of file"); return;

}

{A1,. . . Ak} = N là tập hợp của các các ký hiệu nonterminal và S là ký hiệu bắt đầu. Phương pháp chính là Parse, nó sẽ kiểm tra mà không có đầu vào còn lại sau khi phân tích cú pháp.Nếu phân tích cú pháp thành công, nó gọi lệnh return để thoát khỏi chường trình, nếu không nó ném một ngoại lệ.

Như vậy chúng ta định nghĩa lớp Parser như sau

public class ArithEvalParser {

public double Parse(TokenStream ts) {

ts.MoveNext();

double result = E(ts); switch (ts.Current.kind) {

case Kind.EOF: return result;

default: throw new ApplicationException("Parse error: " + ts.Current); }

}

double E(TokenStream ts) { return Eopt(T(ts), ts); } double Eopt(double inval, TokenStream ts)

{

switch (ts.Current.kind) {

Trang 26

ts.MoveNext(); return Eopt(inval + T(ts), ts); case Kind.MINUS:

ts.MoveNext(); return Eopt(inval - T(ts), ts); default:

return inval; }

}

double T(TokenStream ts) { return Topt(F(ts), ts); } double Topt(double inval, TokenStream ts)

{

switch (ts.Current.kind) {

case Kind.MULT:

ts.MoveNext(); return Topt(inval * F(ts), ts); case Kind.DIV:

ts.MoveNext(); return Topt(inval / F(ts), ts); default: return inval; } } double F(TokenStream ts) { switch (ts.Current.kind) { case Kind.NUM:

double nval = ts.Current.nval; ts.MoveNext(); return nval; case Kind.LPAR:

ts.MoveNext(); double ev = E(ts);

if (ts.Current.kind != Kind.RPAR) {

throw new ApplicationException("Parse error: expected ’)’"); }

ts.MoveNext(); return ev; default:

throw new ApplicationException("Parse error: expected number or ’(’"); }

} }

3. Scanner

Trong trình phân tích cú pháp, đầu vào là một luồng token. Phân tích cú pháp của các tập tin văn bản thường được chia thành hai giai đoạn. Trong giai đoạn đầu, dòng ký tự được chuyển đổi thành luồng token, và thông tin định dạng (chẳng hạn như khoảng trống) trong văn bản đầu vào đã được loại bỏ, quá trình này gọi là phân tích từ vựng.

Trong giai đoạn thứ hai, luồng token được phân tích cú pháp như mô tả trong các phần trước. Việc phân chia thành hai giai đoạn đưa ra một cách thuận tiện để cho phép bất kỳ số lượng khoảng trống giữa các chữ số và tên mà không cho phép khoảng trống bên trong các chữ số và tên. Một khoảng trống, có nghĩa là một ký tự trắng, một ký tự đặc biệt hoặc một ký tự xuống dòng. Scanner sẽ quyết định những ký tự nào là chữ số,

Trang 27

một cái tên, và loại bỏ tất cả khoảng trống dư thừa, trình phân tích cú pháp không tốn thời gian để xử lý các khoảng trống này. Scanner được cài đặt interface Iscanner.

public interface IScanner {

TokenStream Scan(TextReader reader); }

Scanner sẽ không làm việc trực tiếp với các tập tin, nhưng thay vào đó sẽ có một đối tượng reader có kiểu System.IO.TextReader. Điều này cho phép chúng ta tái sử dụng các scanner (và do đó phân tích cú pháp) cho đầu vào không đến từ các tập tin.

public class Scanner : IScanner {

...

public TokenStream Scan(TextReader reader) { while (reader.Peek() != -1) { if (Char.IsWhiteSpace((char)reader.Peek())) { reader.Read(); } else if (Char.IsDigit((char)reader.Peek())) {

yield return Token.FromDouble(ScanReal(reader)); } else { char c = (char)reader.Read(); switch (c) {

case '+': yield return Token.FromKind(Kind.PLUS); break;

case '-': yield return Token.FromKind(Kind.MINUS); break;

case '*': yield return Token.FromKind(Kind.MULT); break;

case '/': yield return Token.FromKind(Kind.DIV); break;

case '(': yield return Token.FromKind(Kind.LPAR); break;

case ')': yield return Token.FromKind(Kind.RPAR); break;

default:

throw new ApplicationException("Illegal

character: ’" + c + "’"); }

} }

yield return Token.FromKind(Kind.EOF); }

}

Chúng ta sử dụng phương thức Char.IsWhiteSpace để kiểm tra một ký tự nhập vào ký tự trắng hay không. Một số phương thức của Char có thể được sử dụng phân loại các ký tự. Ví dụ:

Trang 28

IsDigit kiểm tra ký tự có phải là chữ số thập phân hay không. IsLower kiểm tra xem ký tự có phải là ký tự viết thường không. IsUpper kiểm tra xem ký tự có phải là ký tự viết hoa không. IsLetter kiểm tra xem ký hiệu có phải ký tự hay không.

IsLetterOrDigit kiểm tra ký hiệu có phải là một ký tự hay một ký số không.

Phân biệt tên định danh với những từ khóa trong ngôn ngữ

Hầu hết các ngôn ngữ lập trình đề có những từ khóa dành riêng. Ví dụ C# và Java có từ khóa “class”, “interface”, và “while” …

Trình scanner sẽ phân biệt tên những từ khóa như sau, khi trình scanner phát hiện ra các định danh, nó được so sánh với một danh sách các từ khóa. Nó được phân loại như là một từ khóa nếu nó có trong danh sách, nếu không nó được phân loại là một cái tên.

enum Kind { ..., NAME, CLASS, INTERFACE, WHILE } struct Token { ... }

class Scanner : IScanner {

static string ScanName(TextReader reader) { ... } public IEnumerator<Token> Scan(TextReader reader) { while ( reader.Peek() != -1 ) {

if ( Char.IsWhiteSpace((char) reader.Peek()) ) { reader.Read();

}

else if ( Char.IsLetter((char) reader.Peek()) ) { string sval = ScanName(reader);

switch ( sval ) {

case "class": yield return Token.FromKind(Kind.CLASS); break;

case "interface": yield return Token.FromKind(Kind.INTERFACE); break;

case "while": yield return Token.FromKind(Kind.WHILE); break;

default: yield return Token.FromString(sval); break; }

} else ... }

yield return Token.FromKind(Kind.EOF); }

}

Chữ số là một chuỗi ký tự đại diện một số, chẳng hạn như "3,1414".Chữ số dấu chấm động có thể được mô tả bởi văn phạm này:

Real = Digits "." Digits . Digits = Digit | Digit Digits .

Trang 29

Phương thức ScanReal(reader) được gọi khi trình scanner phát hiện ra ký số c.

static double ScanReal(TextReader reader) { double n = reader.Read() - '0'; while (Char.IsDigit((char)reader.Peek())) n = 10 * n + reader.Read() - '0'; if (reader.Peek() == '.') { reader.Read();

return ScanFrac(n, 0.1, reader); }

else

return n; }

static double ScanFrac(double n, double wt, TextReader reader) { while (Char.IsDigit((char)reader.Peek())) { n += wt * (reader.Read() - '0'); wt /= 10.0; } return n; }

Phương thức ScanReal tích lũy giá trị n của các chữ số trước dấu chẩm cho đến khi phát hiện ra dấu chấm, hoặc là các ký hiệu không phải là ký số được không được đáp ứng, hoặc kết thúc chuỗi đầu vào.Trong trường hợp đầu tiên, phải là một chữ số ngay sau dấu chấm, sau đó phương thức ScanFrac được gọi. Phương pháp ScanFrac tích lũy các giá trị của các chữ số sau dấu chấm cho đến khi các ký hiệu không phải ký số xuất hiện. Sau đó, nó sẽ trả về số đã quét.Các phương thức scanner cho số thực được gọi bằng cách bổ sung thêm một phần vào trong phương pháp Scan. Hơn nữa, các token phải được mở rộng với một thuộc tính nval đễ lưu kiểu số double, tương tự chúng ta cần một giá trị enum thêm kiểu enum Kind.

public TokenStream Scan(TextReader reader) { while (reader.Peek() != -1) { if (Char.IsWhiteSpace((char)reader.Peek())) { reader.Read(); } else if (Char.IsDigit((char)reader.Peek())) {

yield return Token.FromDouble(ScanReal(reader)); }

else { } }

yield return Token.FromKind(Kind.EOF); }

Trang 30

4. Hàm main và chƣơng trình minh họa

static void Main(string[] args) {

IScanner scanner = new Scanner();

ArithEvalParser parser = new ArithEvalParser();

string s = string.Empty;//"(6+3)*2+2*1";

Console.Write("Van pham cho bieu thuc toan hoc:\n");

string grammar = "E = T Eopt ." + System.Environment.NewLine; grammar += "Eopt = \"+\" T Eopt | \"-\" T Eopt | ^ .\n" grammar += "T = F Topt .\n";

grammar += "Topt = \"*\" F Topt | \"/\" F Topt | ^ .\n"; grammar += "F = Real | \"(\" E \")\" .\n";

Console.Write(grammar + System.Environment.NewLine);

while (true) {

Console.Write("Nhap bieu thuc toan hoc:");

s = Console.ReadLine();

TextReader r = new StringReader(s);

TokenStream tokens = scanner.Scan(r);

Console.WriteLine(String.Format("{0}={1}",s,

parser.Parse(tokens))); }

}

Hình 6: Mô tả văn phạm (grammar) của biểu thức toán học

Hàm Main khởi tạo scanner, scanner sẽ phân tích từ vựng và trả kết quả về là các token. Trình phân tích cú pháp sẽ xử lý các token này và tính kết quả của biểu thức toán học.

Trang 31

Nhập biểu thức toán học đơn giản theo quy tắc của văn phạm, trình phân tích sẽ phân tích và tính ra giá trị của biểu thức.

Trang 32

Tài liệu tham khảo

Slide Bài giảng Nguyên Lý Ngôn Ngữ Lập Trình – TS. Nguyễn Tuấn Đăng

Programming Languages: Principles and Paradigms - Maurizio Gabbrielli and Simone Martini

Một phần của tài liệu Mô Tả Ngôn Ngữ Lập Trình Và Viết Chương Trình Xây Dựng Cây Phân Tích Cú Pháp (Trang 28)

Tải bản đầy đủ (PDF)

(36 trang)