Last active
March 18, 2026 13:23
-
-
Save LCamel/f88df3927a0a2aa00b0f83698d36e359 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ❯ 我有 https://github.com/scmlab/gcl-all/wiki/Server-Edit | |
| 我想驗證性質 NextDidChange | |
| claude 先前幫我寫了一版 NextDidChange.csp | |
| 幫我仔細檢查寫的正確性, 是否真的有表達到 wiki 中的意思. | |
| ⏺ Read 1 file (ctrl+o to expand) | |
| ⏺ Fetch(https://github.com/scmlab/gcl-all/wiki/Server-Edit) | |
| ⎿ Received 222.4KB (200 OK) | |
| ⏺ Wiki 的 NextDidChange 已取得,現在仔細對照 CSP 檔案的每個設計決策。 | |
| --- | |
| 檢查結果 | |
| 屬性陳述 ✓ | |
| Wiki 說:「送出的 workspace/applyEdit 如果成功,則 server 收到的下一個 didChange 必屬此 edit」 | |
| CSP 的 SPEC_IDLE [T= SYSTEM_OBS 用 trace refinement 驗證,等價於:「所有可能的執行序列都不違反 SPEC」。方向正確。 | |
| --- | |
| SPEC 狀態機的正確性 | |
| SPEC_IDLE →(apply_sent)→ SPEC_WINDOW | |
| SPEC_WINDOW →(SrvEdit)→ SPEC_IDLE ✓ 成功:NextDidChange 成立 | |
| SPEC_WINDOW →(UsrEdit)→ SPEC_AFTER_REJECT | |
| SPEC_AFTER_REJECT →(apply_sent)→ SPEC_WINDOW ✓ 新的 edit 開新窗口 | |
| SPEC_AFTER_REJECT 沒有 SrvEdit ✓ 舊的 apply 被 reject 後不應有 ghost SrvEdit | |
| 一個值得注意的地方:Wiki 的性質是「如果成功」,CSP 的 SPEC_AFTER_REJECT 是在驗證一個更強的附帶性質——「如果 applyEdit 被 reject(UsrEdit | |
| 先到),之後不會出現 ghost SrvEdit」。這在邏輯上是正確且必要的,但它超出了 wiki 原文性質的字面範圍。這沒有問題,只是要知道它在驗證兩件事: | |
| 1. 成功時 SrvEdit 排在第一 (NextDidChange) | |
| 2. 失敗時不會出現 phantom SrvEdit (系統一致性) | |
| --- | |
| 三個機制的建模 | |
| ┌─────────────────┬──────────────────────────────────────────────────┬─────────────────────────────────────────────────────┬──────────┐ | |
| │ 機制 │ wiki 描述 │ CSP 建模 │ 是否正確 │ | |
| ├─────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────┼──────────┤ | |
| │ Version locking │ applyEdit 帶版本號,版本不符則 reject │ apply_recv?v' -> if v' == v then ... else CLIENT(v) │ ✓ │ | |
| ├─────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────┼──────────┤ | |
| │ Atomic client │ VS Code 單執行緒,check+apply+didChange 不可分割 │ CLIENT 的 apply_recv 處理是一個原子步驟 │ ✓ │ | |
| ├─────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────┼──────────┤ | |
| │ FIFO ordering │ 單 TCP 連線,順序保證 │ BUFFER_EMPTY / BUFFER_FULL 容量為 1 的 FIFO │ ✓ │ | |
| └─────────────────┴──────────────────────────────────────────────────┴─────────────────────────────────────────────────────┴──────────┘ | |
| --- | |
| SERVER 建模 | |
| SERVER_IDLE(v) = | |
| apply_sent!v -> SERVER_PENDING(v) | |
| [] | |
| change.UsrEdit -> SERVER_IDLE(nxt(v)) | |
| SERVER_PENDING(v) = | |
| change.SrvEdit -> SERVER_IDLE(nxt(v)) | |
| [] | |
| change.UsrEdit -> SERVER_IDLE(nxt(v)) | |
| AtMostOnePendingEdit:SERVER_PENDING 不能再送 apply_sent,這正確反映了 wiki 的「pre-send check: reject if pending edit exists」。✓ | |
| 版本追蹤:每收到一個 change(不論 SrvEdit 或 UsrEdit),版本都前進一步。這和客戶端每次 didChange 都 bump version 的行為一致。✓ | |
| --- | |
| 一個潛在的細節問題 | |
| 在 SERVER_PENDING(v) 收到 change.UsrEdit → SERVER_IDLE(nxt(v)) 後,BUFFER 裡面的 apply_recv!v 還沒被 CLIENT 處理(可能還在 buffer 中)。這時 SERVER | |
| 已轉成 IDLE,而 CLIENT 後來收到 apply_recv!v 時,若 CLIENT 已是版本 nxt(v),就會 reject(0 ≠ 1),不送 SrvEdit。這條路徑正確。✓ | |
| --- | |
| 執行工具確認 | |
| .csp + assert ... [T= ... 和 assert ... :[deadlock free [F]] 語法 → 這是標準 FDR4 語法,正確。 | |
| --- | |
| 結論 | |
| CSP 檔案正確地表達了 wiki 中的 NextDidChange 性質,且在以下幾點上還做了更嚴謹的建模: | |
| - 用非同步 BUFFER 讓 reject 路徑可被真正測試到 | |
| - 額外驗證 reject 後不會出現 phantom SrvEdit | |
| - AtMostOnePendingEdit 作為隱含約束嵌入模型中 | |
| ⏺ 可以,而且已經跑完了。結果: | |
| SPEC_IDLE [T= SYSTEM_OBS: Passed | |
| SYSTEM_OBS :[deadlock free [F]]: Passed | |
| 兩個 assertion 都通過: | |
| 1. NextDidChange 性質:SPEC_IDLE [T= SYSTEM_OBS — Passed。系統所有可能的執行序列都滿足 NextDidChange。 | |
| 2. 無 deadlock:SYSTEM_OBS :[deadlock free [F]] — Passed。 | |
| FDR4 搜尋了 39 個狀態、58 個 transition,在幾毫秒內完成。 | |
| 用法就是: | |
| /Applications/FDR4.app/Contents/MacOS/refines NextDidChange.csp |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ✦ 經過仔細檢查 NextDidChange.csp 以及對照 Wiki 中的 Server Edit 說明 (https://github.com/scmlab/gcl-all/wiki/Server-Edit),這份 CSP | |
| 模型非常精確地捕捉了協議的核心機制。 | |
| 驗證結論 | |
| 根據我執行的 FDR4 驗證結果,該模型完全符合預期: | |
| 1. SPEC_IDLE [T= SYSTEM_OBS 透過 (Passed):所有可觀察到的行為路徑都符合 NextDidChange 性質。 | |
| 2. deadlock free 透過 (Passed):系統在任何狀態下都不會卡死,這證明了 Client 與 Server 之間的版本鎖定(Version Locking)邏輯是一致的(否則會出現 | |
| Client 想發送 SrvEdit 但 Server 已因收到 UsrEdit 而拒收的死鎖情況)。 | |
| --- | |
| 詳細檢查與 Wiki 內容對照 | |
| 1. NextDidChange 性質的表達 | |
| Wiki 提到:「成功的 workspace/applyEdit 確保伺服器的下一個 didChange 通知源自該特定編輯。」 | |
| * 模型實作:在 SPEC_WINDOW 狀態(Server 已發出 apply_sent),它只接受 change.SrvEdit(成功)或 change.UsrEdit(遭插隊)。 | |
| * 關鍵邏輯:如果發生了 change.UsrEdit(User 插隊),模型會進入 SPEC_AFTER_REJECT 狀態。在此狀態下,嚴禁出現 | |
| change.SrvEdit。這完美表達了「一旦被插隊,原本的編輯就必須失敗且不可再被應用」的邏輯。 | |
| 2. 版本鎖定機制 (Version Locking) | |
| Wiki 提到:「版本鎖定防止使用者輸入『插隊』。」 | |
| * 模型實作:CLIENT(v) 在收到 apply_recv(v') 時,會檢查 if v' == v。 | |
| * 運作機制:如果使用者先輸入了 (user_type),Client 的版本會跳號。當舊的 apply_recv 到達時,版本不匹配,Client 會保持沉默(不發送 SrvEdit)。這確保了 | |
| Server 端收到的下一個訊息一定是 UsrEdit。 | |
| 3. 佇列與順序 (FIFO & Atomicity) | |
| Wiki 與 CSP 註釋提到 LSP 訊息在單一 TCP 連線中,且 VS Code 是單線程處理。 | |
| * 模型實作:雖然 apply_sent 到 apply_recv 之間有異步 Buffer,但 change 通道被建模為同步(或緊隨 apply_recv 發生),這模擬了 Client | |
| 處理微任務(Microtask)時的原子性:在處理 applyEdit 的瞬間,使用者輸入無法穿插其中。 | |
| 4. 單一待處理編輯 (AtMostOnePendingEdit) | |
| Wiki 提到:「系統限制為單一活動編輯請求。」 | |
| * 模型實作:SERVER_IDLE 狀態允許 apply_sent,但一旦進入 SERVER_PENDING,Server 就無法再發送下一個編輯,直到收到任何 change(無論是 SrvEdit 還是 | |
| UsrEdit)為止。這完全符合 AtMostOnePendingEdit 規則。 | |
| 總結 | |
| 這份 CSP | |
| 寫得非常專業且嚴謹。它不僅驗證了「成功時必須是下一個」,還更進一步驗證了「如果不是下一個(被插隊),則該次編輯絕對不會再成功」,這才是維持分散式狀態 | |
| 一致性的關鍵。 | |
| 目前版本是正確的,可以直接作為 NextDidChange 性質的正式證明模型。 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -- NextDidChange.csp | |
| -- FDR4 model to verify the NextDidChange property of the GCL Server-Edit protocol. | |
| -- | |
| -- Reference: https://github.com/scmlab/gcl-all/wiki/Server-Edit | |
| -- | |
| -- Property (NextDidChange): | |
| -- If workspace/applyEdit succeeds (client applied the edit), | |
| -- then the next textDocument/didChange the server receives | |
| -- belongs to that edit. | |
| -- | |
| -- Assumption (taken as given): | |
| -- VS Code sends all edits from a single applyEdit in one didChange notification. | |
| -- | |
| -- Three mechanisms jointly make this property hold: | |
| -- (1) Version locking – applyEdit carries the expected document version; | |
| -- the client rejects if its version no longer matches. | |
| -- (2) Atomic client – VS Code is single-threaded: version check, edit | |
| -- application, and sending didChange are one microtask. | |
| -- (3) FIFO ordering – LSP messages travel over a single TCP connection; | |
| -- a UsrEdit queued before SrvEdit always arrives first. | |
| -- | |
| -- Previous version used a synchronous apply channel, which forced server and | |
| -- client versions to always stay in sync — making rejection impossible and | |
| -- leaving the SPEC_AFTER_REJECT branch untested. | |
| -- | |
| -- This version models apply delivery as an asynchronous capacity-1 FIFO buffer | |
| -- so that user edits can interleave with in-flight apply messages, properly | |
| -- exercising the version-locking rejection path and SPEC_AFTER_REJECT. | |
| -- =========================================================================== | |
| -- Types | |
| -- =========================================================================== | |
| -- SrvEdit : didChange caused by the server's applyEdit being accepted | |
| -- UsrEdit : didChange caused by direct user input (or a rejected applyEdit) | |
| datatype Tag = SrvEdit | UsrEdit | |
| -- Document version space. N = 3 gives a non-trivial cycle while keeping | |
| -- the state space tractable. | |
| N = 3 | |
| -- =========================================================================== | |
| -- Channels | |
| -- =========================================================================== | |
| -- Server → network buffer : workspace/applyEdit with a version lock | |
| channel apply_sent : {0..N-1} | |
| -- Network buffer → Client : workspace/applyEdit delivered to client | |
| channel apply_recv : {0..N-1} | |
| -- Client → Server : textDocument/didChange | |
| channel change : Tag | |
| -- Internal : user keystroke (synchronises CLIENT and USER) | |
| channel user_type | |
| -- =========================================================================== | |
| -- Helper | |
| -- =========================================================================== | |
| nxt(v) = (v + 1) % N | |
| -- =========================================================================== | |
| -- BUFFER | |
| -- Models a capacity-1 FIFO network between server and client. | |
| -- The server (modelled by SERVER_PENDING) will not emit a second apply_sent | |
| -- before receiving the didChange that resolves the first, so capacity 1 | |
| -- suffices. | |
| -- =========================================================================== | |
| BUFFER_EMPTY = apply_sent?v -> BUFFER_FULL(v) | |
| BUFFER_FULL(v) = apply_recv!v -> BUFFER_EMPTY | |
| -- =========================================================================== | |
| -- CLIENT(v) | |
| -- VS Code, currently at document version v. | |
| -- | |
| -- The client is single-threaded (JS microtask queue). Once it starts | |
| -- handling an applyEdit it atomically: | |
| -- • checks whether v' (the locked version in the request) equals v | |
| -- • if yes : applies the edit, bumps the version, sends didChange(SrvEdit) | |
| -- • if no : silently rejects — no extra didChange is produced | |
| -- =========================================================================== | |
| CLIENT(v) = | |
| -- User keypress: send UsrEdit immediately, bump version | |
| user_type -> change!UsrEdit -> CLIENT(nxt(v)) | |
| [] | |
| -- Receive workspace/applyEdit from buffer | |
| apply_recv?v' -> | |
| (if v' == v | |
| then change!SrvEdit -> CLIENT(nxt(v)) -- version match: apply & confirm | |
| else CLIENT(v)) -- version mismatch: silent reject | |
| -- =========================================================================== | |
| -- USER | |
| -- The user can type at any moment, without bound. | |
| -- =========================================================================== | |
| USER = user_type -> USER | |
| -- =========================================================================== | |
| -- CLIENT_WITH_USER(v) | |
| -- Client and user, synchronising on user_type. | |
| -- =========================================================================== | |
| CLIENT_WITH_USER(v) = CLIENT(v) [| {|user_type|} |] USER | |
| -- =========================================================================== | |
| -- SERVER_IDLE(v) / SERVER_PENDING(v) | |
| -- =========================================================================== | |
| SERVER_IDLE(v) = | |
| apply_sent!v -> SERVER_PENDING(v) | |
| [] | |
| change.UsrEdit -> SERVER_IDLE(nxt(v)) | |
| SERVER_PENDING(v) = | |
| change.SrvEdit -> SERVER_IDLE(nxt(v)) | |
| [] | |
| change.UsrEdit -> SERVER_IDLE(nxt(v)) | |
| -- =========================================================================== | |
| -- SYSTEM | |
| -- | |
| -- Three-party composition: | |
| -- CLIENT_WITH_USER <-(apply_recv)-> BUFFER <-(apply_sent)-> SERVER | |
| -- CLIENT_WITH_USER <-(change)------------------------------> SERVER | |
| -- | |
| -- Step 1: CLIENT_WITH_USER and BUFFER synchronise on apply_recv. | |
| -- Step 2: that sub-system and SERVER synchronise on apply_sent and change. | |
| -- =========================================================================== | |
| CLIENT_BUF = CLIENT_WITH_USER(0) [| {|apply_recv|} |] BUFFER_EMPTY | |
| SYSTEM = CLIENT_BUF [| {|apply_sent, change|} |] SERVER_IDLE(0) | |
| -- Hide internal events so the observable alphabet is | |
| -- {apply_sent.0..N-1, change.SrvEdit, change.UsrEdit}. | |
| SYSTEM_OBS = SYSTEM \ {|user_type, apply_recv|} | |
| -- =========================================================================== | |
| -- Specification: NextDidChange | |
| -- | |
| -- We track three abstract server states: | |
| -- | |
| -- SPEC_IDLE no pending edit | |
| -- SPEC_WINDOW apply_sent was observed; awaiting the first didChange | |
| -- SPEC_AFTER_REJECT the first post-apply didChange was UsrEdit (rejected); | |
| -- no SrvEdit for the old pending edit may appear any more | |
| -- | |
| -- A trace violates NextDidChange iff it reaches SPEC_AFTER_REJECT and then | |
| -- produces change.SrvEdit without an intervening apply_sent. | |
| -- That event is intentionally absent from SPEC_AFTER_REJECT. | |
| -- =========================================================================== | |
| SPEC_IDLE = | |
| apply_sent?_ -> SPEC_WINDOW | |
| [] | |
| change.UsrEdit -> SPEC_IDLE | |
| SPEC_WINDOW = | |
| change.SrvEdit -> SPEC_IDLE -- success: NextDidChange satisfied | |
| [] | |
| change.UsrEdit -> SPEC_AFTER_REJECT -- rejection: monitor for stale SrvEdit | |
| SPEC_AFTER_REJECT = | |
| apply_sent?_ -> SPEC_WINDOW -- fresh applyEdit opens a new window | |
| [] | |
| change.UsrEdit -> SPEC_AFTER_REJECT -- further user edits are fine | |
| -- change.SrvEdit absent: stale SrvEdit here is a NextDidChange violation | |
| -- =========================================================================== | |
| -- Assertions | |
| -- =========================================================================== | |
| -- Primary: every observable behaviour of the system satisfies NextDidChange. | |
| assert SPEC_IDLE [T= SYSTEM_OBS | |
| -- Sanity: the composed system has no deadlocks. | |
| assert SYSTEM_OBS :[deadlock free [F]] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment