摘要:總結做語法解析器錯誤提示功能時,再次刷新了筆者三觀,原來我們以為的必然,在編譯器里對應著那么多可能。語法解析器為了讓報錯符合人們的第一直覺,對錯誤信息做了過濾,只保留剩余數最短的那條錯誤信息。
1 引言
編譯器除了生成語法樹之外,還要在輸入出現錯誤時給出恰當的提示。
比如當用戶輸入 select (name,這是個未完成的 SQL 語句,我們的目標是提示出這個語句未完成,并給出后續的建議: ) - + % / * . ( 。
2 精讀分析一個 SQL 語句,現將 query 字符串轉成 Token 數組,再構造文法樹解析,那么可能出現錯誤的情況有兩種:
語句錯誤。
文法未完成。
給出錯誤提示的第一步是判斷錯誤發生。
通過這張 Token 匹配過程圖可以發現,當深度優先遍歷文法節點時,匹配成功后才會返回父元素繼續往下走。而當走到父元素沒有根節點了才算匹配成功;當嘗試 Chance 時沒有機會了,就是錯誤發生的時機。
所以我們只要找到最后一個匹配成功的節點,再根據最后成功與否,以及搜索出下一個可能節點,就能知道錯誤類型以及給出建議了。
function onMatchNode(matchNode, store) { const matchResult = matchNode.run(store.scanner); if (!matchResult.match) { tryChances(matchNode, store); } else { const restTokenCount = store.scanner.getRestTokenCount(); if (matchNode.matching.type !== "loose") { if (!lastMatch) { lastMatch = { matchNode, token: matchResult.token, restTokenCount }; } } callParentNode(matchNode, store, matchResult.token); } }
所以在運行語法分析器時,在遇到匹配節點(MatchNode)時,如果匹配成功,就記錄下這個節點,這樣我們最終會找到最后一個匹配成功的節點:lastMatch。
之后通過 findNextMatchNodes 函數找到下一個可能的推薦節點列表,作為錯誤恢復的建議。
findNextMatchNodes 函數會根據某個節點,找出下一節點所有可能 Tokens 列表,這個函數后面文章再專門介紹,或者你也可以先閱讀 源碼.語句錯誤
也就是任何一個 Token 匹配失敗。比如:
select * from table_name as table1 error_string;
這里 error_string 就是冗余的語句。
通過語法解析器分析,可以得到執行失敗的結果,然后通過 findNextMatchNodes 函數,我們可以得到下面分析結果:
可以看到,程序判斷出了 error_string 這個 Token 屬于錯誤類型,同時給出建議,可以將 error_string 替換成這 14 個建議字符串中任意一個,都能使語句正確。
之所以失敗類型判斷為錯誤類型,是因為查找了這個正確 Token table1 后面還有一個沒有被使用的 error_string,所以錯誤歸類是 wrong。
注意,這里給出的是下一個 Token 建議,而不是全部 Token 建議,因此推薦了 where 表示 “或者后面跟一個完整的 where 語句”。文法未完成
和語句錯誤不同,這種錯誤所有輸入的單詞都是正確的,但卻沒有寫完。比如:
select *
通過語法解析器分析,可以得到執行失敗的結果,然后通過 findNextMatchNodes 函數,我們可以得到下面分析結果:
可以看到,程序判斷出了 * 這個 Token 屬于未完成的錯誤類型,建議在后面補全這 14 個建議字符串中任意一個。比較容易聯想到的是 where,但也可以是任意子文法的未完成狀態,比如后面補充 , 繼續填寫字段,或者直接跟一個單詞表示別名,或者先輸入 as 再跟別名。
之所以失敗類型判斷為未完成,是因為最后一個正確 Token * 之后沒有 Token 了,但語句解析失敗,那只有一個原因,就是語句為寫完,因此錯誤歸類是 inComplete。
找到最易讀的錯誤類型在一開始有提到,我們只要找到最后一個匹配成功的節點,就可以順藤摸瓜找到錯誤原因以及提示,但最后一個成功的節點可能和我們人類直覺相違背。舉下面這個例子:
select a from b where a = "1" ~ -- 這里手滑了
正常情況,我們都認為錯誤點在 ~,而最后一個正確輸入是 "1"。但詞法解析器可不這么想,在我初版代碼里,判斷出錯誤是這樣的:
提示是 where 錯了,而且提示是 .,有點摸不著頭腦。
讀者可能已經想到了,這個問題與文法結構有關,我們看 fromClause 的文法描述:
const fromClause = () => chain( "from", tableSources, optional(whereStatement), optional(groupByStatement), optional(havingStatement) )();
雖然實際傳入的 where 語句多了一個 ~ 符號,但由于文法認為整個 whereStatement 是可選的,因此出錯后會跳出,跳到 b 的位置繼續匹配,而 顯然 groupByStatement 與 havingStatement 都不能匹配到 where,因此編譯器認為 “不會從 b where a = "1" ~” 開始就有問題吧?因此繼續往回追溯,從 tableName 開始匹配:
const tableName = () => chain([matchWord, chain(matchWord, ".", matchWord)()])();
此時第一次走的 b where a = "1" ~ 路線對應 matchWord,因此嘗試第二條路線,所以認為 where 應該換成 .。
要解決這個問題,首先要 承認這個判斷是對的,因為這是一種 錯誤提前的情況,只是人類理解時往往只能看到最后幾步,所以我們默認用戶想要的錯誤信息,是 正確匹配鏈路最長的那條,并對 onMatchNode 作出下面優化:
將 lastMatch 對象改為 lastMatchUnderShortestRestToken:
if ( !lastMatchUnderShortestRestToken || (lastMatchUnderShortestRestToken && lastMatchUnderShortestRestToken.restTokenCount > restTokenCount) ) { lastMatchUnderShortestRestToken = { matchNode, token: matchResult.token, restTokenCount }; }
也就是每次匹配到正確字符,都獲取剩余 Token 數量,只保留最后一匹配正確 且剩余 Token 最少的那個。
3 總結做語法解析器錯誤提示功能時,再次刷新了筆者三觀,原來我們以為的必然,在編譯器里對應著那么多 “可能”。
當我們遇到一個錯誤 SQL 時,錯誤原因往往不止一個,你可以隨便截取一段,說是從這一步開始就錯了。語法解析器為了讓報錯符合人們的第一直覺,對錯誤信息做了 過濾,只保留剩余 Token 數最短的那條錯誤信息。
4 更多討論討論地址是:精讀《手寫 SQL 編譯器 - 錯誤提示》 · Issue #101 · dt-fe/weekly
如果你想參與討論,請點擊這里,每周都有新的主題,周末或周一發布。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/97314.html
摘要:經過連續幾期的介紹,手寫編譯器系列進入了智能提示模塊,前幾期從詞法到文法語法,再到構造語法樹,錯誤提示等等,都是為智能提示做準備。 1 引言 詞法、語法、語義分析概念都屬于編譯原理的前端領域,而這次的目的是做 具備完善語法提示的 SQL 編輯器,只需用到編譯原理的前端部分。 經過連續幾期的介紹,《手寫 SQL 編譯器》系列進入了 智能提示 模塊,前幾期從 詞法到文法、語法,再到構造語法...
摘要:引言是一個版語法解析器生成器,具有分詞語法樹解析的能力。實現函數用鏈表設計函數是最佳的選擇,我們要模擬調用棧了。但光標所在的位置是期望輸入點,這個輸入點也應該參與語法樹的生成,而錯誤提示不包含光標,所以我們要執行兩次。 1. 引言 syntax-parser 是一個 JS 版語法解析器生成器,具有分詞、語法樹解析的能力。 通過兩個例子介紹它的功能。 第一個例子是創建一個詞法解析器 my...
摘要:返回的語法樹作為結果被傳遞到文法中,其結果可能是。每個元素的子節點全部執行完畢,才會生成當前節點的語法樹。更多討論討論地址是精讀手寫編譯器語法樹如果你想參與討論,請點擊這里,每周都有新的主題,周末或周一發布。 1 引言 重回 手寫 SQL 編輯器 系列。之前幾期介紹了 詞法、文法、語法的解析,以及回溯功能的實現,這次介紹如何生成語法樹。 基于 《回溯》 一文介紹的思路,我們利用 JS ...
閱讀 635·2021-10-27 14:15
閱讀 1162·2021-10-15 09:42
閱讀 2741·2019-08-30 15:53
閱讀 1280·2019-08-23 17:02
閱讀 2955·2019-08-23 16:23
閱讀 3170·2019-08-23 15:57
閱讀 3457·2019-08-23 14:39
閱讀 512·2019-08-23 14:35