Trình soạn thảo dựa trên CDT, Phần 3 : Phân tích cú pháp trên CDT cơ bản

Hiểu trình phân tích cú pháp của CDT và các cây cú pháp trừu tượng của nó

Bài này, là bài thứ ba trong loạt bài gồm năm phần "Trình soạn thảo dựa trên CDT", giới thiệu quá trình phân tích cú pháp được sử dụng bởi công cụ phát triển C/C++ (CDT) của Eclipse. Phân tích cú pháp là một trong những chức năng quan trọng nhất của CDT, nhưng do sự phức tạp của nó mà việc phân tích cú pháp cũng là một trong những khía cạnh ít biết nhất. Nhiều người đã hỏi liệu họ có thể chỉ cần trích nó cho dự án riêng, nhưng ở đây chúng ta sẽ đi xa hơn, sẽ giải thích cách mà các lớp hoạt động và cách làm phù hợp chức năng với CDT thành một khối.

Matthew Scarpino, Phát triển Java, Eclipse Engineering, LLC

Matthew Scarpino là nhà quản lý dự án và nhà phát triển Java tại Eclipse Engineering LLC. Ông là tác giả hàng đầu của SWT/JFace in Action và có đóng góp nhỏ nhưng quan trọng vào Standard Widget Toolkit (SWT). Ông thích nhạc dân gian Ailen, chạy maratông, thơ của William Blake, và Graphical Editing Framework (GEF).



10 09 2010

Tại Phần 2 của loạt bài này, tôi đã giải thích cách trình soạn thảo của CDT cập nhật sự trình bày văn bản của nó bằng cách trả lời cho mỗi động tác gõ phím như thế nào. Nhưng nó làm được nhiều hơn là chỉ đơn giản hiển thị từ khoá với màu sắc và kiểu dáng phông chữ cụ thể: Nó cũng phân tích cấu trúc của mã và theo dõi tất cả các chức năng, câu lệnh và các biến số.

Phân tích này, được gọi là phân tích cú pháp, là chủ đề rộng, tiếp tục cung cấp cho các nhà khoa học máy tính những con đường nghiên cứu mới. Tôi sẽ giải thích ngắn gọn vài lý thuyết phía sau phân tích cú pháp, nhưng tôi sẽ tập trung vào cơ chế hoạt động của CDT. Mục tiêu của tôi là cung cấp đủ thông tin để nếu bạn muốn cải thiện hoặc sửa đổi trình phân tích cú pháp của CDT, thì bạn biết phải thay đổi những lớp và phương thức nào, và cách thay đổi chúng.

Tôi đã thêm những lớp vào công cụ phát triển C/C++ lược giản (BBCDT) để bạn có thể xem cách phân tích cú pháp qua ví dụ. Hầu hết các lớp mới được xử lý bằng quá trình phân tích cú pháp của CDT, vậy bạn sẽ thấy chúng trong trình cắm thêm org.bbcdt.dworks.core (cụ thể là gói org.bbcdt.dworks.core.parser và các gói con của nó).

Điều quan trọng cần nhớ rằng CDT thực sự có hai trình phân tích cú pháp - một trình sử dụng mô hình đối tượng tài liệu vốn có (Persisted Document Object Model-PDOM) và trình kia thì không. Cả hai đều quan trọng đối với CDT phiên bản v3.1 hiện tại, nhưng theo Doug Schaefer thì trình phân tích cú pháp PDOM sẽ dần dần làm nhiệm vụ của trình thứ hai. Tôi sẽ mô tả trình phân tích cú pháp PDOM trong Phần 4, nhưng vì trình phân tích cú pháp thứ hai là dễ hiểu hơn, nên tôi giới thiệu trình phân tích cú pháp đó trong bài này. Cụ thể là tôi sẽ thảo luận về những cái xảy ra trong CDT kể từ lúc Document nhận một cập nhật cho đến khi tạo cây cú pháp trừu tượng mới (Abstract Syntax Tree -AST). Quá trình này có thể được diễn tả tốt nhất trong bốn phần sau:

Đối tượng Reconciler và lớp ReconcilingStrategy của nó
Cách trình phân tích cú pháp nhận các sự kiện từ lớp Document
Xây dựng trình phân tích cú pháp
Cách Trình phân tích cú pháp được tạo và khởi tạo
Quá trình phân tích cú pháp
Cách Trình phân tích cú pháp phân tích cấu trúc của văn bản của WorkingCopy
AST
Mô hình của trình phân tích cú pháp của mã nguồn và cách bạn có thể truy cập nó

Đối tượng Reconciler và lớp ReconcilingStrategy của nó

Phần 2 mô tả cách PresentationReconciler bắt đầu quá trình tạo kiểu dáng văn bản bằng cách trả lời các thay đổi cho tài liệu đã soạn thảo. Tương tự, việc phân tích cú pháp CDT bắt đầu với đối tượng Reconciler nó nghe các sự kiện cùng loại. Điều quan trọng là không được lẫn lộn hai lớp này. Lớp PresentationReconciler cập nhật diện mạo văn bản với mọi động tác phím, và lớp Reconciler chạy theo xử lý thông minh, phân tích tài liệu mà không nắm giữ giao diện người dùng (UI). Đây là lý do khi bạn sử dụng Eclipse, việc tô màu cú pháp hoạt động nhanh hơn nhiều với việc phát hiện lỗi.

Xử lý của Reconciler bắt đầu khi trình nghe của nó phát hiện ra Document mới. Khi người sử dụng cập nhật Document, xử lý này lệnh cho lớp CReconcilingStrategy của mình bắt đầu hoà hợp. Trình điều hoà (Reconciler) là lớp MonoReconciler, (trình điều hoà đơn), có nghĩa có thể chỉ một chiến lược. Ngoài ra, Reconcilerkhông làm gia tăng, tức là chiến lược hoạt động trên toàn bộ Document bất cứ khi nào có thay đổi.

Lớp CReconcilingStrategy truy cập lớp WorkingCopy và lệnh cho nó tổ chức các lớp con trong đối tượng WorkingCopyInfo mà chúng hoạt động như các đối tượng thông tin khác trong mô hình CDT. Nhưng lớp WorkingCopy không có bất kỳ lớp con nào, nó chỉ là bộ nhớ đệm của văn bản phi cấu trúc và không được lưu lại. Để tạo trật tự đối với hỗn loạn này, lớp WorkingCopy gọi phương thức parse() của nó. Cuộc gọi này không bắt đầu quá trình phân tích cú pháp, mà bắt đầu quá trình tạo và khởi tạo trình phân tích cú pháp.


Tạo trình phân tích cú pháp

WorkingCopy bắt đầu việc tạo trình phân tích cú pháp bằng cách kiến thiết đối tượng CModelBuilder để quản lý quá trình phân tích cú pháp. Đối tượng này xác định ngôn ngữ được phân tích từ bản chất dự án và xây dựng bộ đệm ký tự từ văn bản của WorkingCopy. Nó cũng tạo ra một loạt các đối tượng (IProblemRequestor, IScannerInfoProvider, CodeReader, SourceElementRequestorParserLogService) mà nó sử dụng để kiến thiết đối tượng Scanner2 bằng cách gọi phương thức ParserFactory.createScanner(). Phương thức này phân tích các ký tự trong bộ nhớ đệm và cung cấp dấu hiệu cho trình phân tích cú pháp này.

Sử dụng trình phân tích cú pháp ngoài CDT

Có vẻ quá trình tạo trình phân tích cú pháp là phải có, nhưng cần lưu ý điểm quan trọng. Mối quan hệ duy nhất giữa trình phân tích cú pháp và mô hình của CDT là bộ nhớ đệm của các ký tự của WorkingCopy. Do đó, bạn có thể truy cập trình phân tích cú pháp của CDT mà không cần phải sử dụng mọi thứ trong mô hình của CDT. Nhưng vì trình phân tích cú pháp dựa trên DOM tương tác với mô hình CDT, nên bạn không thể thêm trình cắm thêm org.eclipse.cdt.parser vào dự án của mình. Thay vào đó, bạn trích Parser và các lớp mà nó phụ thuộc, rồi gọi các phương thức ParserFactory.createScanner()ParserFactory.createParser() từ bên trong mã của bạn.

Sau khi trình máy quét được tạo ra, lớp CModelBuilder kiến thiết lớp Parser bằng cách gọi phương thức ParserFactory.createParser(). Phương thức này ra lệnh lớp ProblemRequestor bắt đầu cập nhật các chú giải soạn thảo khi gặp lỗi. Sau đó, nó gọi phương thức parse() của Parser.

Đây chính là nơi mà điều thú vị bắt đầu. Nhưng trước khi bàn kỹ hơn, xin cho phép tôi đi chệch chủ đề một chút: bàn về lý thuyết phân tích cú pháp.

Tạm nghỉ: Giới thiệu ngắn về phân tích cú pháp

Ta hãy xem xét câu nhập vào "Matt thích bánh pizza." Bạn có thể biết từng từ riêng lẻ, nhưng nếu bạn không quen với cấu trúc câu: chủ ngữ - động từ - tân ngữ, thì bạn sẽ không hiểu ý nghĩa câu đó. Câu này có ý nghĩa chỉ khi bạn có thể kết hợp các yếu tố câu đầu vào với cấu trúc trừu tượng ("Matt" = chủ ngữ, "thích" = động từ, "bánh pizza" = tân ngữ). Tất nhiên, có rất nhiều loại cấu trúc câu khác và nhiều loại ngôn ngữ khác. Phần thân quy tắc xác định cách các câu đúng được kiến thiết trong một ngôn ngữ đã biết được gọi là văn phạmcủa ngôn ngữ đó.

Với ngôn ngữ lập trình, văn phạm chứa mô hình trừu tượng mà tất cả các đơn vị của mã được lập nên đúng cách (tức các tệp mã nguồn) phải khớp. Bước đầu tiên trong việc khớp này được gọi là quét hoặcphân tích từ vựng. Trình quét đọc các ký tự cá thể của bộ đệm và trả về các kí hiệu (được gọi là dấu hiệu) mà trình phân tích cú pháp sẽ hiểu - chẳng hạn như từ khoá, toán tử và cách chấm câu của ngôn ngữ cụ thể. Thí dụ khi trình quét C đọc các ký tự c, o, n, s,t, nó sẽ trả về đối tượng đánh dấu đơn thể hiện từ khoá const. Trình phân tích cú pháp không quan tâm đến khoảng trắng hoặc các ghi chú, vì vậy trình quét bỏ qua chúng.

Bước thứ hai của quá trình khớp là quá trình phân tích cú pháp. Tại bước này, trình phân tích cú pháp kiểm thử tổ hợp các dấu hiệu để xem cách chúng so sánh với các phần tử trừu tượng trong văn phạm của nó. Nói chung, điều này phức tạp hơn nhiều so với ví dụ chủ ngữ-động từ-tân ngữ trên. Thí dụ nếu câu lệnh bắt đầu với dấu hiệu tương ứng với "enum" (khai báo một liệt kê), thì trình phân tích cú pháp của CDT sẽ cố gắng để so khớp câu lệnh với mô hình trừu tượng của nó ứng với câu liệt kê. Tức là, ở phía sau dấu hiệu ENUM, nó sẽ tìm tên (name) tùy chọn, sau đó là lớp enumerated_list ở giữa các dấu hiệu L_BRACER_BRACEvà lớp enumerated_list cuối cùng. Mỗi thuật ngữ được in nghiêng liên quan đến một phần tử trừu tượng trong văn phạm.

Nhiều trình phân tích cú pháp, kể cả trình phân tích cú pháp CDT, không chỉ đơn thuần kiểm tra những thứ được hình thành tốt. Sau khi phân tích cú pháp đầu vào, chúng lưu trữ các thông tin cấu trúc của mình dưới một hình thức thích hợp để phân tích, lập chỉ mục và tìm kiếm. Hình thức này thường ở dạng cây, gọi là AST. Thí dụ nếu bạn tạo dấu hiệu một bài của trang developerWorks và gửi nó qua một trình phân tích cú pháp tiếng Anh, nó có thể tạo ra một cấu trúc hình cây như trong hình 1.

Hình 1. Một ví dụ về AST
Một ví dụ về AST

Như bạn có thể thấy, các phần tử trong cây là khái quát ở phần đỉnh (developerWorks Article Bài báo của trang developerWorks) và càng cụ thể hơn tại mỗi nút đi xuống. Nếu tôi có không gian thì sẽ chỉ ra cách các nút ở phía dưới chứa các câu và cuối cùng là các từ cụ thể. Đối với ngôn ngữ lập trình, các nút đầu cuối này chứa từ khoá riêng lẻ, toán tử và các biến. Việc tìm một hàm hay tên biến nhờ tìm kiếm AST sẽ dễ dàng hơn nhiều so với phân tích lại toàn bộ tài liệu. Bây giờ tôi sẽ chỉ cho bạn cách mà trình phân tích cú pháp CDT tạo AST và cách bạn có thể duyệt nó với mã của riêng bạn.


Quá trình phân tích cú pháp

Tôi đã không thấy bất kỳ tệp văn phạm nào cho CDT, nhưng chúng ta có thể xác định các phần tử của mô hình trừu tượng của nó từ phương thức trong lớp Parser và từ các ghi chú trước những phương thức này. Lớp này đặt trong gói org.dworks.bbcdt.internal.core.parser. Như đã đề cập, phương thức phân tích cú pháp đầu tiên là translationUnit() vì phần tử TranslationUnit thể hiện toàn bộ tệp mã nguồn, giống như phần tử Article trong ví dụ của tôi thể hiện toàn bộ bài viết.

Câu lệnh khó hiểu trong các ghi chú xuất hiện trước phương thức translationUnit() :

 translationUnit : (declaration)*

Nếu bạn quen với dạng BNF mở rộng (Extended Backus Naur -EBNF), thì bạn biết quy tắc này có nghĩa rằng phần tử TranslationUnit bao gồm số bất kỳ các phần tử Declaration. Các ghi chú này không cung cấp văn phạm CDT đầy đủ, nhưng nếu bạn cố gắng sử dụng hoặc chỉnh sửa lớp Parser, bạn sẽ thấy có ích.

Do phần tử Declaration ở ngay phía dưới đối tượng TranslationUnit, nên phương thức translationUnit() gọi phương thức declaration() Quy tắc EBNF cho chúng ta biết một khai báo có thể nhận một trong sáu dạng sau:

Câu lệnh Assembly
Sau dấu hiệu asm là phần tử ASMDefinition
Câu lệnh Namespace
Sau dấu hiệu namespace là phần tử NamespaceDefinition
Câu lệnh Using
Sau dấu hiệu using là phần tử UsingDeclaration
Câu lệnh template
Sau dấu hiệu hay xuất khẩu (export) là phần tử TemplateDeclaration
Câu lệnh linkage
Sau dấu hiệu bên ngoài extern là phần tử LinkageSpecification
Câu lệnh simple
Phần tử SimpleDeclaration

Để xác định loại khai báo đã có, phương thức declaration() sử dụng câu lệnh chuyển đổi switch có khai thác phụ thuộc vào loại của dấu hiệu tiếp theo của trình quét. Mỗi dấu hiệu thực hiện IToken và lưu loại của nó (là số nguyên từ 1 đến 141), tệp đang quét và khoảng trống và chiều dài của nó trong Document được quét.

Nếu khai báo không thuộc một trong năm loại đầu tiên, thì phương thức simpleDeclaration() được gọi, cùng với một phỏng đoán về bản chất của khai báo. Phỏng đoán đầu tiên là khai báo là một trình kiến thiết, nhưng nếu phương thức đưa ra một lớp BackTrackException, thì phỏng đoán tiếp là khai báo về chức năng. Nếu phỏng đoán đó không đúng, phương thức được gọi lần thứ ba, lần này phỏng đoán đó là một khai báo về biến. Nếu phỏng đoán này đưa ra một ngoại lệ, phương thức trả về rỗng.

Trình phân tích cú pháp tiếp tục gọi phương thức để làm khớp mã với các phần tử mô hình, nhưng độ sâu của phân tích phụ thuộc vào chế độ của nó. Năm chế độ phân tích cú pháp là:

QUICK_PARSE
Không phân tích các chức năng bên trong hoặc các tệp đi kèm
STRUCTURAL_PARSE
Không phân tích các chức năng bên trong nhưng phân tích các tệp đi kèm
COMPLETE_PARSE
Phân tích các chức năng bên trong, và các tệp đi kèm
COMPLETION_PARSE
Phân tích các chức năng bên trong và tệp đi kèm, dừng lại ở khoảng trống và tối ưu hóa tra cứu truy vấn theo biểu tượng
SELECTION_PARSE
Phân tích các chức năng bên trong và tệp đi kèm, dừng tại các khoảng trống và cung cấp thông tin ngữ nghĩa về phạm vi được lựa chọn.

Chế độ cũng xác định đối tượng gọi lại nào mà lớp Parser sử dụng để lưu thông tin khi phân tích cú pháp. Trong chế độ QUICK_PARSE, lớp QuickParseCallback theo dõi các macro, chức năng, các khai báo và bất kỳ lỗi nào. Trong bất kỳ chế độ khác, lớp StructuralParseCallback lưu giữ các thông tin bổ sung đó như phạm vi hiện tại của Parsercác biến, các câu lệnh của không gian tên, các khai báo liệt kê… Nhưng bởi vì việc phân tích bổ sung cần nhiều thời gian, nên QUICK_PARSE là chế độ mặc định của trình phân tích cú pháp.

Dù đối tượng gọi lại mà bạn sử dụng là gì, các thông tin quan trọng nhất mà nó mang theo là lớp ASTCompilationUnit. Tôi sẽ mô tả ý nghĩa của điều này sau đây.


AST

Phương thức phân tích cú pháp đầu tiên, translationUnit(), bắt đầu hoạt động bằng cách tạo đối tượng ASTCompilationUnit. Đối tượng này không chỉ là nút cao nhất trong AST mà còn là hộp chứa tất cả các nút dưới nó. Khi trình phân tích cú pháp nhận ra một khai báo trong chương trình nguồn, nó sẽ thêm lớp con của ASTDeclaration vào danh sách các khai báo ASTCompilationUnit. Nếu khai báo đầu tiên trong tệp nguồn là typedef, thì phần tử đầu tiên trong danh sách của ASTCompilationUnit sẽ là một ASTTypedefDeclaration.

Tương tự, mỗi đối tượng ASTDeclaration lưu giữ các đối tượng AST đã hình thành khai báo. Thí dụ một khai báo chức năng được thể hiện qua ASTFunction, chứa các đối tượng AST thể hiện kiểu trả về, các thông số, và bất kỳ chức năng trong nó. Khi trình phân tích cú pháp kết thúc việc phân tích của mình, thì ASTCompilationUnit sẽ chứa một AST hoàn thiện thể hiện cấu trúc tệp. Vào thời điểm đó, việc tìm thấy phần tử cụ thể trong tệp cũng đơn giản như thuật toán duyệt cây.


Cập nhật BBCDT

Nếu mở dự án BBCDT trong Eclipse, bạn sẽ thấy có rất nhiều mã, đặc biệt trong dự án trình cắm thêm org.dworks.bbcdt.core. Nhiều lớp mới này cần thiết để thiết lập và thực hiện phân tích cú pháp. Nhưng phần lớn chúng thể hiện các phần tử của tệp nguồn C/C++ hoặc AST của C/C++.

Trình cắm thêm của lớp org.dworks.bbcdt.ui cũng có các lớp mới. Hầu hết chúng liên quan đến quá trình điều hợp, nhưng lớp ASTAction trong gói org.dworks.bbcdt.ui.action thực hiện giao diện IEditorActionDelegate. Nó xuất hiện khi trình soạn thảo BBCDT nhìn thấy. Khi bạn nhấn vào thanh công cụ của nó, sẽ truy cập IASTCompilationUnit và lặp lại thông qua các đối tượng khai báo của nó. Nếu bất kỳ trình cắm thêm nào khai báo các chức năng hoặc các biến hoặc bắt đầu với không gian tên, thì đối tượng ASTAction đang sử dụng, hay ở bên ngoài sẽ hiển cửa sổ liệt kê kiểu của mỗi khai báo với thông tin cụ thể cho kiểu khai báo đó. Liệt kê 1 cho thấy đoạn mã làm cho khả năng này có thể.

Liệt kê 1. Truy cập các khai báo trong AST của CDT
IASTCompilationUnit unit = CCorePlugin.getCompilationUnit();
Iterator iter = unit.getDeclarations(); 

while(iter.hasNext()) { 
  IASTDeclaration decl = (IASTDeclaration)iter.next(); 

  if (decl instanceof IASTFunction) 
     output += "Function declaration: " + 
         ((IASTFunction)decl).getName(); 

  else if (decl instanceof IASTLinkageSpecification) 
      output += "Linkage declaration: " +
          ((IASTLinkageSpecification)decl).getLinkageString(); 
       
  else if (decl instanceof IASTNamespaceDefinition) 
      output += "Namespace definition: " +
           ((IASTNamespaceDefinition)decl).getName(); 
            
  else if (decl instanceof IASTUsingDeclaration) 
      output += "Using declaration: " +
           ((IASTUsingDeclaration)decl).getName(); 

   else if (decl instanceof IASTVariable)
        output += "Variable declaration: " + 
           ((IASTVariable)decl).getName();

Bạn sẽ không tìm thấy mã này tại bất cứ nơi nào trong CDT; tôi đã đặt mã đó lại với nhau để dễ dàng truy cập và duyệt IASTCompilationUnit. Bằng cách thay đổi ASTAction, bạn có thể tìm kiếm thông qua các nút và tạo toàn bộ cây. Hình 2 cho thấy kết quả đầu ra của BBCDT sau khi bạn nhấn vào nút ASTAction .

Hình 2. Kết quả đầu ra của phân tích AST trong BBCDT
Kết quả đầu ra của phân tích AST trong BBCDT

Kết luận

Bài này giải thích cách trình phân tích cú pháp của CDT đáp ứng với những thay đổi trong văn bản của trình soạn thảo, rồi tạo một AST với các phần tử C/C++. Bài cũng tóm tắt lý thuyết chung về phân tích cú pháp. Nhưng có nhiều thứ hơn nữa để phân tích cú pháp; hãy xem mục Tài nguyên để có cuốn sách giáo khoa về phân tích cú pháp trực tuyến tuyệt vời.

Trình phân tích cú pháp mà tôi đã mô tả có mục đích của nó, nhưng cũng có những nhược điểm. Nó chậm và việc phân tích của nó bị giới hạn ở một tệp nguồn tại một thời điểm. Trong phần 4, CDT đã kết hợp với một trình phân tích cú pháp khác mạnh hơn và phức tạp hơn.


Các tải về

Mô tảTênKích thước
Sample plug-in filesos-ecl-cdt3-BBCDT_Plugins.zip955KB
Sample plug-in filesos-ecl-cdt3-BBCDT_Projects.zip2MB

Tài nguyên

Học tập

Lấy sản phẩm và công nghệ

Thảo luận

Bình luận

developerWorks: Đăng nhập

Các trường được đánh dấu hoa thị là bắt buộc (*).


Bạn cần một ID của IBM?
Bạn quên định danh?


Bạn quên mật khẩu?
Đổi mật khẩu

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Ở lần bạn đăng nhập đầu tiên vào trang developerWorks, một hồ sơ cá nhân của bạn được tạo ra. Thông tin trong bản hồ sơ này (tên bạn, nước/vùng lãnh thổ, và tên cơ quan) sẽ được trưng ra cho mọi người và sẽ đi cùng các nội dung mà bạn đăng, trừ khi bạn chọn việc ẩn tên cơ quan của bạn. Bạn có thể cập nhật tài khoản trên trang IBM bất cứ khi nào.

Thông tin gửi đi được đảm bảo an toàn.

Chọn tên hiển thị của bạn



Lần đầu tiên bạn đăng nhập vào trang developerWorks, một bản trích ngang được tạo ra cho bạn, bạn cần phải chọn một tên để hiển thị. Tên hiển thị của bạn sẽ đi kèm theo các nội dung mà bạn đăng tải trên developerWorks.

Tên hiển thị cần có từ 3 đến 30 ký tự. Tên xuất hiện của bạn phải là duy nhất trên trang Cộng đồng developerWorks và vì lí do an ninh nó không phải là địa chỉ email của bạn.

Các trường được đánh dấu hoa thị là bắt buộc (*).

(Tên hiển thị cần có từ 3 đến 30 ký tự)

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Thông tin gửi đi được đảm bảo an toàn.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=70
Zone=Nguồn mở
ArticleID=518121
ArticleTitle=Trình soạn thảo dựa trên CDT, Phần 3 : Phân tích cú pháp trên CDT cơ bản
publish-date=09102010