Skip to content

Instantly share code, notes, and snippets.

@LCamel
Last active March 18, 2026 13:23
Show Gist options
  • Select an option

  • Save LCamel/f88df3927a0a2aa00b0f83698d36e359 to your computer and use it in GitHub Desktop.

Select an option

Save LCamel/f88df3927a0a2aa00b0f83698d36e359 to your computer and use it in GitHub Desktop.
❯ 我有 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
✦ 經過仔細檢查 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 性質的正式證明模型。
-- 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