五月二十九日。封校第三十七天。
趙強是被走廊里的喊聲吵醒的。
"解封快了!連續兩天零新增了!"
他從上鋪探出頭,看見隔壁寢室幾個人在走廊里蹦來蹦去。
林遠已經醒了,靠在床頭看論文:"29號如果還是零,30號下午學校會發通知。"
"那不就是明天?"
"看數據。"
蘇明遠在對面床上翻了個身:"三十七天了……我連校門朝哪邊開都快忘了。"
趙強沒接話。他已經翻身下床,筆記本攤在桌上,翻到第四條。
字符串閉合。
他盯著這四個字看了很久。
前三個規則的難點完全不同。分號檢測靠狀態機區分上下文——一行代碼該不該有分號,取決於當前處於什麼狀態。變量檢測靠作用域樹跨行追蹤——變量在哪裡聲明、在哪裡使用、在哪裡失效。括號匹配靠棧結構處理嵌套——左括號進右括號出,類型必須匹配。
字符串閉合靠什麼?
他先寫了一行最簡單的代碼:
// c
char *s = "hello world;
缺了結尾的引號。人眼一掃就知道。但程序怎麼判斷?
最簡單的思路——數引號。一行里引號出現奇數次就是未閉合。
他立刻否定了自己。
// c
char *s = "he said "hello"";
三個雙引號,但字符串是閉合的。中間兩個"是轉義的,不算起止標記。
"數引號不行。得追蹤狀態。"
他又想到了狀態機。分號檢測的時候林遠教過——五種狀態,根據當前字符決定轉換。但那個狀態機只有五種狀態,轉換條件也簡單。字符串閉合的狀態追蹤要複雜得多。
他在紙上畫起來。
四個狀態:普通代碼、雙引號字符串內部、單引號字符常量內部、轉義狀態。轉換條件逐一列出——普通代碼遇到"進入字符串,字符串里再遇到"退出。反斜槓進入轉義狀態,轉義完回退到之前的狀態。
行末判斷:狀態如果還在字符串內部,就是未閉合。
畫完基本圖,他又想到了續行符:
// c
char *s = "hello
world";
第一行末尾的反斜槓表示續行。字符串要到第二行末尾的引號才閉合。這種情況第一行行末不能報錯。
他加了一個續行狀態:行末遇到不檢查閉合,把字符串狀態保留到下一行。
五個狀態。轉換條件清晰。邊界處理完整。
然後他列邊界用例。這次他沒急著寫代碼——變量檢測的時候他吃過虧,初版寫完才發現邊界情況沒考慮全。現在他養成了習慣:先把所有要處理的場景列出來,預期輸出想清楚,再動手。
1. 普通字符串未閉合
2. 字符常量未閉合
3. 轉義引號不算閉合
4. 連續轉義:"pathtofile"
5. 注釋里的引號:// "這不是字符串"
6. 字符串里包含單引號:"it's fine"
7. 續行字符串
8. 行末只有反斜槓
八個用例。每個的預期輸出都寫在旁邊。
代碼結構比分號檢測複雜——轉義狀態需要記住"從哪來的",才能轉義結束後回退。他用了一個prev_state變量記錄進入轉義之前的狀態。
四十分鐘寫完。
跑前五個用例。全部通過。
第六個出了bug。
"it's fine"——字符串里的單引號被當成字符常量的起始標記,狀態直接亂了。後續的雙引號被匹配進了字符常量,整個判斷崩潰。
他盯著屏幕看了十幾秒。
問題出在狀態轉換的優先級。當處於雙引號字符串狀態時,遇到單引號不應該切換到字符常量狀態——因為單引號在字符串內部只是一個普通字符。
他在狀態轉換函數裡加了一個判斷:只有在"普通代碼"狀態下,引號才能觸發狀態切換。在"雙引號字符串"狀態下,只有雙引號(非轉義的)能觸發閉合,單引號當作普通字符處理。
改完重跑。八個用例全部通過。
趙強沒有立刻喊"過了"——不像分號檢測那次,整層樓都聽見他叫。他只是靠在椅背上,長出了一口氣,然後把八個用例的輸入輸出在紙上一字一句核對了一遍。
林遠注意到了這個變化。半年前的趙強過了測試會跳起來喊。現在的趙強過了測試會安靜地覆核。不是不興奮了,是興奮的方式變了——從外放變成內斂。
"什麼感覺?"林遠問。
"比括號匹配有意思。"趙強說,"括號匹配是進一個出一個的對稱結構,字符串閉合是'進去就得出來'的追蹤問題。看起來簡單,但轉義字符、續行、注釋里的假引號——全是陷阱。"
"那個bug——單引號被誤判——你覺得根本原因是什麼?"
"狀態轉換的上下文約束。"趙強說,"不同狀態下規則不一樣。在普通代碼下遇到任何引號都要處理,但在字符串狀態下只有匹配的引號才有意義。"他頓了一下,"跟分號檢測一個道理——不同狀態不同規則。但prev_state和作用域樹的parent指針其實是一個思路:記錄'從哪來的',才能知道'往哪去'。"
林遠點了一下頭,沒有多說。
他注意到一個變化——趙強解決這個問題的過程中,沒有問過他一次。從分析到畫圖到寫代碼到修bug,全程獨立。
分號檢測的時候需要教狀態機概念,變量檢測的時候需要點一個key設計的細節。但字符串閉合——一個字都沒問。
趙強已經開始學會把不同的知識串聯起來,自己解決複雜問題了。
下午,食堂門口貼了一張通知——"關於做好解除封閉管理準備工作的通知"。
趙強拎著饅頭回來:"看見沒?"
"看見了。"
"做好準備工作——就是馬上要解除了唄?"
"等最後一天數據確認。30號還是零的話,下午發正式通知。"
趙強咬著饅頭,有點恍惚:"三十七天了。你還記得剛封校那會兒嗎?超市泡麵被搶光了,你囤了二十包。"
"記得。"
"那時候你說最多封兩個月。結果三十七天差不多了。"
"我說的是'大概率不會超過兩個月'。"
"行行行。那解封之後你第一件事幹什麼?"
"回家。"
"第二件呢?"
"去騰迅。"
"第三件呢?"
"吃涮羊肉。蘇明遠說了請客的。"
蘇明遠晚上回來帶了個消息。
"WatchGuard海淀那個客戶——上次說沒預算的——簽了。二十個節點,年費制。"
"案例簡報起效果了?"
"他們技術總監原話——'你們是我們看過的第一家能提供完整行業案例的安全公司'。"蘇明遠坐下來,"案例簡報這個思路是對的。客戶不在乎技術參數有多漂亮,他在乎的是'跟我一樣的人用了效果怎麼樣'。"
"其他行業加快。"
"已經在做了。金融和教育好了,醫療正在寫。"
窗外傳來遠處的聲音——有人在唱歌。不知道哪個寢室,隔著窗戶聽不真切,但旋律很熟悉。
趙強側耳聽了一會兒:"《七里香》?"
"嗯。"
"這歌都火到別的樓層了。"
林遠沒說話。六首歌的進度一直在推——七里香和江南錄完音,童話demo錄完,一千年以後和歐若拉編曲定稿,寧夏還在排期。封校把這些事拖慢了,但沒拖停。
遠處的歌聲漸漸停了。校園重新安靜下來。
十一點,燈滅了。
趙強翻到筆記本第五條——注釋閉合。
"這個和字符串閉合有點像。"他自言自語,"都是追蹤開-關狀態。區別是注釋的標記是/*和*/,兩個字符不是單個。而且注釋裡面沒有轉義。"
他想了想:"難點在//和/*的區分。單行注釋不需要閉合,多行注釋才需要。遇到/之後要看下一個字符是什麼。"
"明天能搞定嗎?"
"能。"
安靜了一會兒。
"你知道這五條做完意味著什麼嗎?"趙強說,"墨鋒的語法檢查引擎,基礎框架搭完了。分號、變量、括號、字符串、注釋——這五條覆蓋了C語言最常見的錯誤類型。後面十三條都是在框架上擴展,引擎核心不用再改。"
又安靜了一會兒。
"說真的,"趙強的聲音變輕了,"剛來北達的時候你讓我寫狀態機,我其實不太懂。現在回頭看——狀態機、作用域樹、棧結構、引號狀態追蹤——每個東西單獨拿出來都要想很久,但現在能自己把這些組合起來解決新問題了。"
安靜了一會兒。
"五條做完,墨鋒的語法檢查引擎基礎框架就搭完了。"趙強說,"後面十三條都是在框架上擴展,核心不用再改。"
又安靜了一會兒。
"明天搞注釋閉合,後天解封。"趙強說。
"嗯。"
趙強笑了。
封校第三十七天。SARS連續第二天零新增。趙強字符串閉合全程獨立完成——五個狀態、八個邊界用例、一次bug修復。WatchGuard案例簡報產出新訂單。校園門口貼了解封準備通知。
明天,門會打開。