Câu trả lời:
Có ba lựa chọn thực sự, cả ba đều thích hợp hơn trong các tình huống khác nhau.
Giả sử, bạn được yêu cầu xây dựng trình phân tích cú pháp cho một số định dạng dữ liệu cổ NGAY BÂY GIỜ. Hoặc bạn cần trình phân tích cú pháp của bạn để được nhanh chóng. Hoặc bạn cần trình phân tích cú pháp của bạn để có thể dễ dàng bảo trì.
Trong những trường hợp này, có lẽ bạn tốt nhất nên sử dụng trình tạo trình phân tích cú pháp. Bạn không cần phải tìm hiểu chi tiết, bạn không cần phải có nhiều mã phức tạp để hoạt động chính xác, bạn chỉ cần viết ra ngữ pháp mà đầu vào sẽ tuân thủ, viết một số mã xử lý và trình phân tích cú pháp tức thì.
Những lợi thế rất rõ ràng:
Có một điều bạn phải cẩn thận với trình tạo phân tích cú pháp: đôi khi có thể từ chối ngữ pháp của bạn. Để biết tổng quan về các loại trình phân tích cú pháp khác nhau và cách chúng có thể cắn bạn, bạn có thể muốn bắt đầu ở đây . Ở đây bạn có thể tìm thấy một cái nhìn tổng quan về rất nhiều triển khai và các loại ngữ pháp mà họ chấp nhận.
Trình tạo phân tích cú pháp rất hay, nhưng chúng không thân thiện với người dùng (người dùng cuối chứ không phải bạn). Bạn thường không thể đưa ra thông báo lỗi tốt, cũng như không thể cung cấp phục hồi lỗi. Có lẽ ngôn ngữ của bạn rất kỳ lạ và các trình phân tích cú pháp từ chối ngữ pháp của bạn hoặc bạn cần kiểm soát nhiều hơn trình tạo cho bạn.
Trong những trường hợp này, sử dụng trình phân tích cú pháp đệ quy gốc viết tay có lẽ là tốt nhất. Mặc dù việc xử lý đúng có thể phức tạp, bạn có toàn quyền kiểm soát trình phân tích cú pháp của mình để bạn có thể thực hiện tất cả các loại nội dung hay mà bạn không thể làm với trình tạo trình phân tích cú pháp, như thông báo lỗi và thậm chí khôi phục lỗi (thử xóa tất cả dấu chấm phẩy khỏi tệp C # : trình biên dịch C # sẽ khiếu nại, nhưng dù sao cũng sẽ phát hiện ra hầu hết các lỗi khác bất kể sự hiện diện của dấu chấm phẩy).
Các trình phân tích cú pháp viết tay cũng thường hoạt động tốt hơn các trình phân tích cú pháp được tạo ra, giả sử chất lượng của trình phân tích cú pháp là đủ cao. Mặt khác, nếu bạn không quản lý để viết một trình phân tích cú pháp tốt - thường là do (sự kết hợp) thiếu kinh nghiệm, kiến thức hoặc thiết kế - thì hiệu suất thường chậm hơn. Đối với các từ vựng thì ngược lại là đúng: các từ vựng được tạo ra thường sử dụng tra cứu bảng, làm cho chúng nhanh hơn (hầu hết) các văn bản viết tay.
Giáo dục, viết trình phân tích cú pháp của riêng bạn sẽ dạy cho bạn nhiều hơn là sử dụng một trình tạo. Rốt cuộc, bạn phải viết mã ngày càng phức tạp hơn, cộng với việc bạn phải hiểu chính xác cách bạn phân tích một ngôn ngữ. Mặt khác, nếu bạn muốn học cách tạo ngôn ngữ của riêng mình (vì vậy, hãy có kinh nghiệm về thiết kế ngôn ngữ), thì tùy chọn 1 hoặc tùy chọn 3 là thích hợp hơn: nếu bạn đang phát triển một ngôn ngữ, nó có thể sẽ thay đổi rất nhiều, và tùy chọn 1 và 3 cho bạn thời gian dễ dàng hơn với điều đó.
Đây là con đường tôi hiện đang đi xuống: bạn viết trình tạo trình phân tích cú pháp của riêng bạn . Mặc dù rất không cần thiết, nhưng làm điều này có thể sẽ dạy cho bạn nhiều nhất.
Để cho bạn biết những gì làm một dự án như thế này liên quan đến tôi sẽ cho bạn biết về tiến trình của riêng tôi.
Trình tạo lexer
Tôi đã tạo trình tạo lexer của riêng mình trước. Tôi thường thiết kế phần mềm bắt đầu bằng cách sử dụng mã, vì vậy tôi đã nghĩ về cách tôi muốn có thể sử dụng mã của mình và viết đoạn mã này (nó ở C #):
Lexer<CalculatorToken> calculatorLexer = new Lexer<CalculatorToken>(
new List<StringTokenPair>()
{ // This is just like a lex specification:
// regex token
new StringTokenPair("\\+", CalculatorToken.Plus),
new StringTokenPair("\\*", CalculatorToken.Times),
new StringTokenPair("(", CalculatorToken.LeftParenthesis),
new StringTokenPair(")", CalculatorToken.RightParenthesis),
new StringTokenPair("\\d+", CalculatorToken.Number),
});
foreach (CalculatorToken token in
calculatorLexer.GetLexer(new StringReader("15+4*10")))
{ // This will iterate over all tokens in the string.
Console.WriteLine(token.Value);
}
// Prints:
// 15
// +
// 4
// *
// 10
Các cặp mã thông báo chuỗi đầu vào được chuyển đổi thành cấu trúc đệ quy tương ứng mô tả các biểu thức chính quy mà chúng thể hiện bằng cách sử dụng các ý tưởng của ngăn xếp số học. Điều này sau đó được chuyển đổi thành NFA (automaton hữu hạn hữu hạn), sau đó được chuyển đổi thành DFA (automaton hữu hạn xác định). Sau đó, bạn có thể kết hợp các chuỗi với DFA.
Bằng cách này, bạn sẽ có được một ý tưởng tốt về cách chính xác các từ vựng hoạt động. Ngoài ra, nếu bạn thực hiện đúng cách, kết quả từ trình tạo lexer của bạn có thể nhanh như triển khai chuyên nghiệp. Bạn cũng không mất bất kỳ biểu cảm nào so với tùy chọn 2 và không có nhiều biểu cảm so với tùy chọn 1.
Tôi đã triển khai trình tạo lexer của mình chỉ trong hơn 1600 dòng mã. Mã này làm cho công việc trên, nhưng nó vẫn tạo ra lexer khi bạn khởi động chương trình: Tôi sẽ thêm mã để ghi nó vào đĩa vào một lúc nào đó.
Nếu bạn muốn biết làm thế nào để viết lexer của riêng bạn, đây là một nơi tốt để bắt đầu.
Trình tạo trình phân tích cú pháp
Sau đó, bạn viết trình tạo trình phân tích cú pháp của bạn. Tôi đề cập ở đây một lần nữa để biết tổng quan về các loại trình phân tích cú pháp khác nhau - như một quy tắc chung, họ càng có thể phân tích cú pháp, chúng càng chậm.
Tốc độ không phải là vấn đề đối với tôi, tôi đã chọn triển khai trình phân tích cú pháp Earley. Việc triển khai nâng cao của trình phân tích cú pháp Earley đã được chứng minh là chậm hơn khoảng hai lần so với các loại trình phân tích cú pháp khác.
Đổi lại với tốc độ đó, bạn có khả năng phân tích bất kỳ loại ngữ pháp nào, thậm chí là mơ hồ. Điều này có nghĩa là bạn không bao giờ phải lo lắng về việc trình phân tích cú pháp của bạn có bất kỳ đệ quy trái nào trong đó hay không, hoặc xung đột giảm ca là gì. Bạn cũng có thể xác định ngữ pháp dễ dàng hơn bằng cách sử dụng các ngữ pháp mơ hồ nếu kết quả là cây phân tích cú pháp nào không quan trọng, chẳng hạn như bạn phân tích 1 + 2 + 3 như (1 + 2) +3 hay 1 + (2 + 3).
Đây là những gì một đoạn mã sử dụng trình tạo trình phân tích cú pháp của tôi có thể trông như sau:
Lexer<CalculatorToken> calculatorLexer = new Lexer<CalculatorToken>(
new List<StringTokenPair>()
{
new StringTokenPair("\\+", CalculatorToken.Plus),
new StringTokenPair("\\*", CalculatorToken.Times),
new StringTokenPair("(", CalculatorToken.LeftParenthesis),
new StringTokenPair(")", CalculatorToken.RightParenthesis),
new StringTokenPair("\\d+", CalculatorToken.Number),
});
Grammar<IntWrapper, CalculatorToken> calculator
= new Grammar<IntWrapper, CalculatorToken>(calculatorLexer);
// Declaring the nonterminals.
INonTerminal<IntWrapper> expr = calculator.AddNonTerminal<IntWrapper>();
INonTerminal<IntWrapper> term = calculator.AddNonTerminal<IntWrapper>();
INonTerminal<IntWrapper> factor = calculator.AddNonTerminal<IntWrapper>();
// expr will be our head nonterminal.
calculator.SetAsMainNonTerminal(expr);
// expr: term | expr Plus term;
calculator.AddProduction(expr, term.GetDefault());
calculator.AddProduction(expr,
expr.GetDefault(),
CalculatorToken.Plus.GetDefault(),
term.AddCode(
(x, r) => { x.Result.Value += r.Value; return x; }
));
// term: factor | term Times factor;
calculator.AddProduction(term, factor.GetDefault());
calculator.AddProduction(term,
term.GetDefault(),
CalculatorToken.Times.GetDefault(),
factor.AddCode
(
(x, r) => { x.Result.Value *= r.Value; return x; }
));
// factor: LeftParenthesis expr RightParenthesis
// | Number;
calculator.AddProduction(factor,
CalculatorToken.LeftParenthesis.GetDefault(),
expr.GetDefault(),
CalculatorToken.RightParenthesis.GetDefault());
calculator.AddProduction(factor,
CalculatorToken.Number.AddCode
(
(x, s) => { x.Result = new IntWrapper(int.Parse(s));
return x; }
));
IntWrapper result = calculator.Parse("15+4*10");
// result == 55
(Lưu ý rằng IntWrapper chỉ đơn giản là một Int32, ngoại trừ C # yêu cầu nó phải là một lớp, do đó tôi phải giới thiệu một lớp bao bọc)
Tôi hy vọng bạn thấy rằng đoạn mã trên rất mạnh mẽ: bất kỳ ngữ pháp nào bạn có thể đưa ra đều có thể được phân tích cú pháp. Bạn có thể thêm các đoạn mã tùy ý trong ngữ pháp có khả năng thực hiện nhiều nhiệm vụ. Nếu bạn quản lý để làm cho tất cả điều này hoạt động, bạn có thể sử dụng lại mã kết quả để thực hiện rất nhiều nhiệm vụ rất dễ dàng: chỉ cần tưởng tượng xây dựng một trình thông dịch dòng lệnh bằng cách sử dụng đoạn mã này.
Nếu bạn chưa bao giờ, đã từng viết một trình phân tích cú pháp, tôi sẽ khuyên bạn nên làm điều đó. Thật thú vị, và bạn học được cách mọi thứ hoạt động, và bạn học cách đánh giá cao nỗ lực mà trình phân tích cú pháp và trình tạo từ vựng tiết kiệm cho bạn khỏi làm lần sau khi bạn cần một trình phân tích cú pháp.
Tôi cũng đề nghị bạn thử đọc http://compilers.iecc.com/crenshaw/ vì nó có thái độ rất thực tế đối với cách thực hiện.
Ưu điểm của việc viết trình phân tích cú pháp gốc đệ quy của riêng bạn là bạn có thể tạo các thông báo lỗi chất lượng cao về các lỗi cú pháp. Sử dụng trình tạo trình phân tích cú pháp, bạn có thể tạo các lỗi sản xuất và thêm thông báo lỗi tùy chỉnh tại một số điểm nhất định, nhưng trình tạo trình phân tích cú pháp không phù hợp với khả năng kiểm soát hoàn toàn việc phân tích cú pháp.
Một lợi thế khác của việc viết của riêng bạn là dễ dàng phân tích thành một cách trình bày đơn giản hơn mà không có sự tương ứng 1-1 với ngữ pháp của bạn.
Nếu ngữ pháp của bạn đã được sửa và các thông báo lỗi rất quan trọng, hãy xem xét việc tự lăn hoặc ít nhất là sử dụng trình tạo trình phân tích cú pháp cung cấp cho bạn các thông báo lỗi bạn cần. Nếu ngữ pháp của bạn liên tục thay đổi, bạn nên xem xét sử dụng trình tạo phân tích cú pháp thay thế.
Bjarne Stroustrup nói về cách anh ta sử dụng YACC cho lần triển khai C ++ đầu tiên (xem Thiết kế và tiến hóa của C ++ ). Trong trường hợp đầu tiên, anh ta muốn anh ta viết trình phân tích cú pháp gốc đệ quy của riêng mình!
Tùy chọn 3: Không (Cuộn trình tạo trình phân tích cú pháp của riêng bạn)
Chỉ vì có lý do để không sử dụng ANTLR , bison , Coco / R , Grammatica , JavaCC , Lemon , Parboiled , SableCC , Quex , v.v. - điều đó không có nghĩa là bạn nên ngay lập tức cuộn trình phân tích cú pháp + lexer của riêng bạn.
Xác định lý do tại sao tất cả các công cụ này không đủ tốt - tại sao chúng không cho phép bạn đạt được mục tiêu của mình?
Trừ khi bạn chắc chắn rằng những điểm kỳ lạ trong ngữ pháp mà bạn đang xử lý là duy nhất, bạn không nên tạo một trình phân tích cú pháp tùy chỉnh + từ vựng cho nó. Thay vào đó, hãy tạo một công cụ sẽ tạo ra những gì bạn muốn, nhưng cũng có thể được sử dụng để đáp ứng các nhu cầu trong tương lai, sau đó phát hành dưới dạng Phần mềm miễn phí để ngăn chặn những người khác gặp vấn đề tương tự như bạn.
Cuộn trình phân tích cú pháp của riêng bạn buộc bạn phải suy nghĩ trực tiếp về sự phức tạp của ngôn ngữ của bạn. Nếu ngôn ngữ khó phân tích, có lẽ nó sẽ khó hiểu.
Có rất nhiều sự quan tâm đến các trình tạo phân tích cú pháp trong những ngày đầu, được thúc đẩy bởi cú pháp ngôn ngữ rất phức tạp (một số người sẽ nói "bị tra tấn"). JOVIAL là một ví dụ đặc biệt tồi tệ: nó yêu cầu hai biểu tượng nhìn vào thời điểm mà mọi thứ khác yêu cầu nhiều nhất là một biểu tượng. Điều này làm cho việc tạo trình phân tích cú pháp cho trình biên dịch JOVIAL trở nên khó khăn hơn mong đợi (vì General Dynamics / Fort Worth Division đã học được cách khó khăn khi họ mua trình biên dịch JOVIAL cho chương trình F-16).
Ngày nay, gốc đệ quy là phương thức phổ biến, bởi vì nó dễ dàng hơn cho các nhà văn biên dịch. Trình biên dịch gốc đệ quy thưởng mạnh mẽ cho thiết kế ngôn ngữ đơn giản, gọn gàng, trong đó việc viết một trình phân tích cú pháp gốc đệ quy cho một ngôn ngữ đơn giản, gọn gàng hơn là một ngôn ngữ lộn xộn, lộn xộn.
Cuối cùng: Bạn đã cân nhắc việc nhúng ngôn ngữ của mình vào LISP và để một thông dịch viên LISP thực hiện công việc nặng nhọc cho bạn chưa? AutoCAD đã làm điều đó, và thấy nó làm cho cuộc sống của họ dễ dàng hơn rất nhiều. Có khá nhiều trình thông dịch LISP nhẹ ngoài kia, một số có thể nhúng.
Tôi đã viết một trình phân tích cú pháp cho ứng dụng thương mại một lần và tôi đã sử dụng yacc . Có một nguyên mẫu cạnh tranh trong đó một nhà phát triển đã viết toàn bộ bằng tay trong C ++ và nó hoạt động chậm hơn khoảng năm lần.
Đối với lexer cho trình phân tích cú pháp này, tôi đã viết nó hoàn toàn bằng tay. Phải mất - xin lỗi, nó đã gần như 10 năm trước đây, vì vậy tôi không nhớ nó một cách chính xác - khoảng 1000 dòng trong C .
Lý do tại sao tôi viết lexer bằng tay là ngữ pháp đầu vào của trình phân tích cú pháp. Đó là một yêu cầu, một cái gì đó mà việc triển khai trình phân tích cú pháp của tôi phải tuân thủ, trái ngược với thứ tôi thiết kế. (Tất nhiên tôi sẽ thiết kế nó theo cách khác. Và tốt hơn!) Ngữ pháp phụ thuộc hoàn toàn vào ngữ cảnh và thậm chí từ vựng phụ thuộc vào ngữ nghĩa ở một số nơi. Ví dụ, dấu chấm phẩy có thể là một phần của mã thông báo ở một nơi, nhưng dấu phân cách ở một nơi khác - dựa trên cách giải thích ngữ nghĩa của một số yếu tố đã được phân tích cú pháp trước đó. Vì vậy, tôi đã "chôn vùi" những phụ thuộc ngữ nghĩa như vậy trong từ vựng viết tay và điều đó đã để lại cho tôi một BNF khá đơn giản , dễ thực hiện trong yacc.
THÊM để đáp ứng với Macneil : yacc cung cấp một sự trừu tượng hóa rất mạnh mẽ cho phép lập trình viên suy nghĩ về các thiết bị đầu cuối, không phải thiết bị đầu cuối, sản xuất và những thứ tương tự. Ngoài ra, khi triển khai yylex()
chức năng, nó giúp tôi tập trung vào việc trả lại mã thông báo hiện tại và không lo lắng về những gì trước hoặc sau nó. Lập trình viên C ++ làm việc ở cấp độ ký tự, không có lợi ích của sự trừu tượng hóa đó và cuối cùng tạo ra một thuật toán phức tạp hơn và kém hiệu quả hơn. Chúng tôi kết luận rằng tốc độ chậm hơn không liên quan gì đến chính C ++ hoặc bất kỳ thư viện nào. Chúng tôi đã đo tốc độ phân tích cú pháp thuần túy với các tệp được tải trong bộ nhớ; nếu chúng tôi gặp sự cố đệm tệp, yacc sẽ không phải là công cụ được chúng tôi lựa chọn để giải quyết.
CSONG MUỐN THÊM : đây không phải là một công thức để viết các trình phân tích cú pháp nói chung, chỉ là một ví dụ về cách nó hoạt động trong một tình huống cụ thể.
Điều đó phụ thuộc hoàn toàn vào những gì bạn cần phân tích. Bạn có thể tự lăn nhanh hơn bạn có thể đạt được mục đích học tập của một người từ chối không? Các công cụ được phân tích cú pháp tĩnh có đủ để bạn không hối hận về quyết định này không? Bạn có thấy việc triển khai hiện tại quá phức tạp không? Nếu vậy, hãy vui vẻ tự lăn, nhưng chỉ khi bạn không thực hiện một lộ trình học tập.
Gần đây, tôi thực sự thích trình phân tích cú pháp chanh , được cho là đơn giản và dễ dàng nhất mà tôi từng sử dụng. Vì mục đích làm cho mọi thứ dễ bảo trì, tôi chỉ sử dụng nó cho hầu hết các nhu cầu. SQLite sử dụng nó cũng như một số dự án đáng chú ý khác.
Nhưng, tôi hoàn toàn không hứng thú với các từ vựng, ngoài việc họ không cản trở tôi khi tôi cần sử dụng một (do đó, chanh). Bạn có thể, và nếu vậy, tại sao không làm cho một? Tôi có cảm giác bạn sẽ quay lại sử dụng một thứ tồn tại, nhưng hãy gãi ngứa nếu bạn phải :)
Nó phụ thuộc vào mục tiêu của bạn là gì.
Bạn đang cố gắng học cách trình phân tích cú pháp / trình biên dịch làm việc? Sau đó viết của riêng bạn từ đầu. Đó là cách duy nhất bạn thực sự học để đánh giá cao tất cả những gì họ đang làm. Tôi đã viết một vài tháng qua, và đó là một trải nghiệm thú vị và có giá trị, đặc biệt là 'ah, vì vậy đó là lý do tại sao ngôn ngữ X thực hiện điều này ...'.
Bạn có cần nhanh chóng kết hợp một cái gì đó cho một ứng dụng đúng hạn không? Sau đó, có lẽ sử dụng một công cụ phân tích cú pháp.
Bạn có cần một cái gì đó mà bạn muốn mở rộng trong vòng 10, 20, thậm chí 30 năm tới không? Viết của riêng bạn, và dành thời gian của bạn. Nó sẽ có giá trị nó.
Bạn đã xem xét cách tiếp cận bàn làm việc ngôn ngữ Martin Fowlers ? Trích dẫn từ bài báo
Sự thay đổi rõ ràng nhất mà bàn làm việc ngôn ngữ tạo ra cho phương trình là sự dễ dàng tạo DSL bên ngoài. Bạn không còn phải viết một trình phân tích cú pháp. Bạn phải xác định cú pháp trừu tượng - nhưng đó thực sự là một bước mô hình hóa dữ liệu khá đơn giản. Ngoài ra, DSL của bạn có được một IDE mạnh mẽ - mặc dù bạn phải dành một chút thời gian để xác định trình soạn thảo đó. Máy phát điện vẫn là thứ bạn phải làm và ý nghĩa của tôi là nó không dễ hơn bao giờ hết. Nhưng sau đó, xây dựng một máy phát cho DSL tốt và đơn giản là một trong những phần dễ nhất của bài tập.
Đọc điều đó, tôi sẽ nói rằng những ngày viết trình phân tích cú pháp của riêng bạn đã kết thúc và tốt hơn là sử dụng một trong những thư viện có sẵn. Khi bạn đã thành thạo thư viện thì tất cả các DSL mà bạn tạo trong tương lai đều được hưởng lợi từ kiến thức đó. Ngoài ra, những người khác không phải học cách tiếp cận của bạn để phân tích cú pháp.
Chỉnh sửa để bao gồm nhận xét (và câu hỏi sửa đổi)
Ưu điểm của việc tự lăn
Vì vậy, trong ngắn hạn, bạn nên tự lăn lộn khi bạn muốn thực sự hack sâu vào ruột của một vấn đề khó khăn nghiêm trọng mà bạn cảm thấy có động lực mạnh mẽ để làm chủ.
Ưu điểm của việc sử dụng thư viện của người khác
Do đó, nếu bạn muốn có kết quả nhanh chóng, hãy sử dụng thư viện của người khác.
Nhìn chung, điều này dẫn đến một sự lựa chọn về mức độ bạn muốn sở hữu vấn đề, và do đó là giải pháp. Nếu bạn muốn tất cả thì hãy cuộn của riêng bạn.
Lợi thế lớn để viết của riêng bạn là bạn sẽ biết cách viết của riêng bạn. Lợi thế lớn khi sử dụng một công cụ như yacc là bạn sẽ biết cách sử dụng công cụ này. Tôi là một fan hâm mộ của ngọn cây cho khám phá ban đầu.
Tại sao không rẽ nhánh một trình tạo trình phân tích cú pháp nguồn mở và biến nó thành của riêng bạn? Nếu bạn không sử dụng trình tạo trình phân tích cú pháp, mã của bạn sẽ rất khó duy trì, nếu bạn thực hiện thay đổi lớn cú pháp ngôn ngữ của mình.
Trong các trình phân tích cú pháp của mình, tôi đã sử dụng các biểu thức chính quy (ý tôi là kiểu Perl) để mã hóa và sử dụng một số hàm tiện lợi để tăng khả năng đọc mã. Tuy nhiên, một mã phân tích cú pháp tạo có thể nhanh hơn bằng cách làm cho bảng trạng thái và dài switch
- case
s, có thể làm tăng kích thước mã nguồn trừ khi bạn .gitignore
cho họ.
Đây là hai ví dụ về trình phân tích cú pháp tùy chỉnh bằng văn bản của tôi:
https://github.com/SHiNKiROU/DesignScript - một phương ngữ BASIC, vì tôi quá lười để viết lookahead trong ký hiệu mảng, tôi đã hy sinh chất lượng thông báo lỗi https://github.com/SHiNKiROU/ExprParser - Một máy tính công thức. Lưu ý các thủ thuật siêu lập trình kỳ lạ
"Tôi có nên sử dụng 'bánh xe' đã thử và thử lại này không?"