Skip to main content

msime_engine/
diagnostics.rs

1//! Every user-visible diagnostic and error message, verbatim from the C++ engine and bridge. Goldens record diagnostics and host tests compare error text, so modules use these constants instead of writing their own wording. Messages specific to one module's validation (personal dictionary edits, snapshot staging) live with that module but follow the same rule: copy the C++ text.
2
3// ---- KeyResult diagnostics (session, nine-key) ----
4pub const ENGLISH_WORD_NOT_LEARNED: &str = "English word could not be learned.";
5pub const FREQUENCY_NOT_PERSISTED: &str = "Unable to persist candidate frequency adjustment.";
6pub const ENGLISH_FREQUENCY_NOT_PERSISTED: &str =
7    "English candidate frequency could not be persisted.";
8pub const PIN_NOT_PERSISTED: &str = "Unable to persist the pinned candidate.";
9pub const REMOVAL_NOT_PERSISTED: &str = "Unable to persist candidate removal.";
10pub const POSITION_NOT_PERSISTED: &str = "Unable to persist candidate position.";
11pub const PHRASE_NOT_PERSISTED: &str = "Unable to persist the composed phrase.";
12pub const SENTENCE_NOT_PERSISTED: &str = "Unable to persist the selected sentence.";
13pub const TYPO_NOT_PERSISTED: &str = "Typo correction could not be persisted.";
14pub const AUTOCORRECT_PREFERENCE_NOT_PERSISTED: &str =
15    "Autocorrect preference could not be persisted.";
16pub const PICK_TRANSITION_NOT_PERSISTED: &str = "Unable to persist the pick transition.";
17pub const PERSONAL_CONTEXT_NOT_PERSISTED: &str = "Unable to persist personal context.";
18pub const NINE_KEY_FREQUENCY_NOT_PERSISTED: &str =
19    "Unable to persist nine-key candidate frequency adjustment.";
20pub const NINE_KEY_REMOVAL_NOT_PERSISTED: &str = "Unable to persist nine-key candidate removal.";
21pub const NINE_KEY_POSITION_NOT_PERSISTED: &str = "Unable to persist nine-key candidate position.";
22pub const NINE_KEY_DIGIT_LIMIT: &str = "一次最多输入 48 个按键,请先选择候选";
23
24// ---- Local mode query diagnostics ----
25pub const QUICK_PHRASE_UNAVAILABLE: &str = "Quick phrase database is unavailable.";
26pub const QUICK_PHRASE_QUERY_FAILED: &str = "Quick phrase database could not be queried.";
27pub const EMOJI_UNAVAILABLE: &str = "Emoji database is unavailable.";
28pub const EMOJI_QUERY_FAILED: &str = "Emoji database could not be queried.";
29pub const KAOMOJI_UNAVAILABLE: &str = "Kaomoji database is unavailable.";
30pub const KAOMOJI_QUERY_FAILED: &str = "Kaomoji database could not be queried.";
31pub const SUPER_JIANPIN_UNAVAILABLE: &str = "Super-jianpin database is unavailable.";
32pub const SUPER_JIANPIN_QUERY_FAILED: &str = "Super-jianpin database could not be queried.";
33
34// ---- Engine errors (Session construction, runtime paths) ----
35pub const INVALID_SESSION_OPTIONS: &str = "Invalid session options";
36pub const UNKNOWN_HELPCODE_SCHEMA: &str = "Unknown helpcode schema";
37pub const INVALID_PUNCTUATION_LOCK: &str = "Invalid punctuation lock";
38pub const RUNTIME_DIRECTORIES_MUST_BE_ABSOLUTE: &str = "Runtime directories must be absolute";
39pub const RUNTIME_ASSET_PATH_ESCAPES: &str = "Runtime asset path escapes its directory";
40pub const INVALID_RUNTIME_CONTENT_ID: &str = "Invalid runtime content ID";
41pub const RUNTIME_ROOTS_OVERLAP: &str =
42    "Resource, user-data and cache directories must not overlap";
43pub const INCOMPLETE_RUNTIME_GENERATION: &str = "Incomplete runtime dictionary generation";
44/// Followed by the replay error.
45pub const RUNTIME_REPLAY_FAILED: &str = "Runtime dictionary replay failed: ";
46/// Followed by the source path.
47pub const RUNTIME_COPY_FAILED: &str = "Unable to copy runtime dictionary: ";
48pub const RUNTIME_FINALIZE_FAILED: &str = "Cannot finalize runtime dictionary generation";
49/// 后面接文件名和原因,见 `format::check_format_version`。
50pub const DICTIONARY_FORMAT_UNSUPPORTED: &str = "Dictionary format version unsupported: ";
51pub const INVALID_DICTIONARY_STATE: &str = "Invalid or unavailable dictionary state";
52/// 拿不到词库的独占维护租约(`DictionaryAccess::try_maintenance`):还有会话或另一次维护开着。错误码是 `ErrorCode::Busy`。
53pub const DICTIONARY_MAINTENANCE_BUSY: &str = "Dictionary maintenance busy";
54
55// ---- Host facade errors (the C++ bridge) ----
56pub const CHARACTER_MUST_BE_ASCII: &str = "Engine character must be ASCII";
57pub const PUNCTUATION_MUST_BE_ASCII: &str = "Engine punctuation must be ASCII";
58pub const PAIRED_OPENING_MUST_BE_ASCII: &str = "Paired punctuation opening must be ASCII";
59pub const UNSUPPORTED_INPUT_COMMAND: &str = "Unsupported input command";
60pub const INVALID_CANDIDATE_POSITION: &str = "Invalid candidate position";
61pub const INVALID_CANDIDATE_EDGE: &str = "Invalid candidate edge";
62pub const UNSUPPORTED_INPUT_SCHEME: &str = "Unsupported input scheme";
63/// 方案合法,但不在会话的 `enabled_schemes` 里(例如五笔版的 Engine 被要求切到全拼)。
64pub const INPUT_SCHEME_NOT_ENABLED: &str = "Input scheme is not enabled";
65pub const UNSUPPORTED_SHUANGPIN_PROFILE: &str = "Unsupported shuangpin profile";
66/// 双拼方案是 `custom`,但没有给表或表不合法;消息后面接着具体原因(`shuangpin::custom::CustomProfileError`)。
67pub const INVALID_CUSTOM_SHUANGPIN_PROFILE: &str = "Invalid custom shuangpin profile";
68pub const UNSUPPORTED_WUBI_PROFILE: &str = "Unsupported wubi profile";
69pub const UNSUPPORTED_FREQUENCY_MODE: &str = "Unsupported frequency mode";
70pub const UNSUPPORTED_VIETNAMESE_METHOD: &str = "Unsupported Vietnamese input method";
71pub const UNSUPPORTED_VIETNAMESE_TONE_STYLE: &str = "Unsupported Vietnamese tone style";
72pub const UNSUPPORTED_KOREAN_LAYOUT: &str = "Unsupported Korean layout";
73pub const UNSUPPORTED_DICTIONARY_KIND: &str = "Unsupported dictionary kind";
74pub const EXPECTED_AT_MOST_ONE_ENTRY: &str = "Expected at most one dictionary entry";
75pub const EXPECTED_AT_MOST_ONE_WEIGHT: &str = "Expected at most one weight";
76pub const TRANSLATION_SIDECAR_FAILED: &str = "Unable to prepare custom translation sidecar";
77pub const RESET_IN_PLACE: &str = "Cannot reset packaged dictionaries in place";
78pub const PACKAGED_DICTIONARY_UNAVAILABLE: &str = "Packaged dictionary is unavailable";
79pub const RESET_JOURNAL_FAILED: &str = "Cannot prepare empty learning journal";
80pub const RESET_STAGE_DICTIONARIES_FAILED: &str = "Cannot stage fresh dictionaries";
81pub const RESET_STAGE_FAILED: &str = "Cannot stage learned-data reset";
82pub const RESET_PUBLISH_FAILED: &str = "Cannot publish learned-data reset";
83pub const INVALID_SNAPSHOT_RECORD_LIMIT: &str = "Invalid snapshot record limit";
84pub const INVALID_SNAPSHOT_RECORD_TYPE: &str = "Invalid snapshot record type";
85pub const INVALID_SNAPSHOT_DICTIONARY_KIND: &str = "Invalid snapshot dictionary kind";
86pub const INVALID_SNAPSHOT_POSITION: &str = "Invalid snapshot position";
87pub const INVALID_SNAPSHOT_SELECTION_COUNT: &str = "Invalid snapshot selection count";
88pub const SNAPSHOT_STREAM_FAILED: &str = "Snapshot record stream failed";
89pub const ENGLISH_DICTIONARY_UNAVAILABLE: &str = "English dictionary unavailable";
90pub const INVALID_ENGLISH_COMPLETION_LIMIT: &str = "Invalid English completion limit";
91pub const INVALID_ENGLISH_COMPLETION_PREFIX: &str = "Invalid English completion prefix";
92pub const CANDIDATE_GLOSS_UNAVAILABLE: &str = "Candidate gloss dictionary unavailable";
93pub const OFFLINE_GLOSS_UNAVAILABLE: &str = "Offline gloss dictionary unavailable";
94pub const OFFLINE_GLOSS_UNREADABLE: &str = "Offline gloss dictionary unreadable";
95pub const OFFLINE_GLOSS_VERSION_UNSUPPORTED: &str = "Offline gloss dictionary version unsupported";
96pub const OFFLINE_GLOSS_LANGUAGE_MISMATCH: &str = "Offline gloss dictionary language mismatch";
97pub const OFFLINE_GLOSS_READ_FAILED: &str = "Offline gloss dictionary read failed";
98pub const PRONUNCIATION_UNAVAILABLE: &str = "Pronunciation dictionary unavailable";
99pub const PRONUNCIATION_UNREADABLE: &str = "Pronunciation dictionary unreadable";
100pub const PRONUNCIATION_VERSION_UNSUPPORTED: &str = "Pronunciation dictionary version unsupported";
101pub const PRONUNCIATION_KIND_MISMATCH: &str = "Pronunciation dictionary kind mismatch";
102pub const PRONUNCIATION_READ_FAILED: &str = "Pronunciation dictionary read failed";
103pub const LANGUAGE_DICTIONARY_UNAVAILABLE: &str = "Language dictionary unavailable";
104pub const LANGUAGE_DICTIONARY_VERSION_UNSUPPORTED: &str = "Language dictionary version unsupported";
105pub const INVALID_EMOJI_CATALOG_PAGE: &str = "Invalid emoji catalog page";
106pub const INVALID_EMOJI_CATALOG_CURSOR: &str = "Invalid emoji catalog cursor";
107pub const EMOJI_CATALOG_UNAVAILABLE: &str = "Emoji catalog unavailable";
108pub const EMOJI_CATALOG_QUERY_UNAVAILABLE: &str = "Emoji catalog query unavailable";
109pub const EMOJI_CATALOG_QUERY_REJECTED: &str = "Emoji catalog query rejected";
110pub const EMOJI_CATALOG_READ_FAILED: &str = "Emoji catalog read failed";
111
112// ---- 诊断码 ----
113
114/// `KeyResult::diagnostic` 的稳定数字码(`KeyResult::diagnostic_code`),供宿主和 C ABI 按数字分支,不必比对说明文字。说明文字仍是上面那些常量,原样保留。编号一经发布只追加、不改、不复用;新加一条 KeyResult 诊断时在 [`DIAGNOSTIC_CODES`] 末尾接一个新号。
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
116#[repr(u16)]
117pub enum DiagnosticCode {
118    EnglishWordNotLearned = 1,
119    FrequencyNotPersisted = 2,
120    EnglishFrequencyNotPersisted = 3,
121    PinNotPersisted = 4,
122    RemovalNotPersisted = 5,
123    PositionNotPersisted = 6,
124    PhraseNotPersisted = 7,
125    SentenceNotPersisted = 8,
126    TypoNotPersisted = 9,
127    AutocorrectPreferenceNotPersisted = 10,
128    PickTransitionNotPersisted = 11,
129    PersonalContextNotPersisted = 12,
130    NineKeyFrequencyNotPersisted = 13,
131    NineKeyRemovalNotPersisted = 14,
132    NineKeyPositionNotPersisted = 15,
133    NineKeyDigitLimit = 16,
134    QuickPhraseUnavailable = 17,
135    QuickPhraseQueryFailed = 18,
136    EmojiUnavailable = 19,
137    EmojiQueryFailed = 20,
138    KaomojiUnavailable = 21,
139    KaomojiQueryFailed = 22,
140    SuperJianpinUnavailable = 23,
141    SuperJianpinQueryFailed = 24,
142    /// 九键笔画筛选拿不到笔画词库(`LANGUAGE_DICTIONARY_UNAVAILABLE`)。这条常量放在宿主门面错误那段,因为它同时是查语言词库接口的错误文字。
143    LanguageDictionaryUnavailable = 25,
144    /// 不在映射表里的说明文字:注音的选字与按键把底层错误原文(`EngineError` 的 `Display`)放进诊断,这类文字没有自己的码。
145    Other = 0xFFFF,
146}
147
148/// 诊断码与说明文字的映射表,KeyResult 诊断常量各占一行,按编号排。
149pub const DIAGNOSTIC_CODES: [(DiagnosticCode, &str); 25] = [
150    (
151        DiagnosticCode::EnglishWordNotLearned,
152        ENGLISH_WORD_NOT_LEARNED,
153    ),
154    (
155        DiagnosticCode::FrequencyNotPersisted,
156        FREQUENCY_NOT_PERSISTED,
157    ),
158    (
159        DiagnosticCode::EnglishFrequencyNotPersisted,
160        ENGLISH_FREQUENCY_NOT_PERSISTED,
161    ),
162    (DiagnosticCode::PinNotPersisted, PIN_NOT_PERSISTED),
163    (DiagnosticCode::RemovalNotPersisted, REMOVAL_NOT_PERSISTED),
164    (DiagnosticCode::PositionNotPersisted, POSITION_NOT_PERSISTED),
165    (DiagnosticCode::PhraseNotPersisted, PHRASE_NOT_PERSISTED),
166    (DiagnosticCode::SentenceNotPersisted, SENTENCE_NOT_PERSISTED),
167    (DiagnosticCode::TypoNotPersisted, TYPO_NOT_PERSISTED),
168    (
169        DiagnosticCode::AutocorrectPreferenceNotPersisted,
170        AUTOCORRECT_PREFERENCE_NOT_PERSISTED,
171    ),
172    (
173        DiagnosticCode::PickTransitionNotPersisted,
174        PICK_TRANSITION_NOT_PERSISTED,
175    ),
176    (
177        DiagnosticCode::PersonalContextNotPersisted,
178        PERSONAL_CONTEXT_NOT_PERSISTED,
179    ),
180    (
181        DiagnosticCode::NineKeyFrequencyNotPersisted,
182        NINE_KEY_FREQUENCY_NOT_PERSISTED,
183    ),
184    (
185        DiagnosticCode::NineKeyRemovalNotPersisted,
186        NINE_KEY_REMOVAL_NOT_PERSISTED,
187    ),
188    (
189        DiagnosticCode::NineKeyPositionNotPersisted,
190        NINE_KEY_POSITION_NOT_PERSISTED,
191    ),
192    (DiagnosticCode::NineKeyDigitLimit, NINE_KEY_DIGIT_LIMIT),
193    (
194        DiagnosticCode::QuickPhraseUnavailable,
195        QUICK_PHRASE_UNAVAILABLE,
196    ),
197    (
198        DiagnosticCode::QuickPhraseQueryFailed,
199        QUICK_PHRASE_QUERY_FAILED,
200    ),
201    (DiagnosticCode::EmojiUnavailable, EMOJI_UNAVAILABLE),
202    (DiagnosticCode::EmojiQueryFailed, EMOJI_QUERY_FAILED),
203    (DiagnosticCode::KaomojiUnavailable, KAOMOJI_UNAVAILABLE),
204    (DiagnosticCode::KaomojiQueryFailed, KAOMOJI_QUERY_FAILED),
205    (
206        DiagnosticCode::SuperJianpinUnavailable,
207        SUPER_JIANPIN_UNAVAILABLE,
208    ),
209    (
210        DiagnosticCode::SuperJianpinQueryFailed,
211        SUPER_JIANPIN_QUERY_FAILED,
212    ),
213    (
214        DiagnosticCode::LanguageDictionaryUnavailable,
215        LANGUAGE_DICTIONARY_UNAVAILABLE,
216    ),
217];
218
219impl DiagnosticCode {
220    pub const fn value(self) -> u16 {
221        self as u16
222    }
223
224    /// 一条 KeyResult 诊断文字对应的码;表外的文字是 [`DiagnosticCode::Other`]。
225    pub fn from_message(message: &str) -> Self {
226        DIAGNOSTIC_CODES
227            .iter()
228            .find(|(_, text)| *text == message)
229            .map_or(Self::Other, |(code, _)| *code)
230    }
231
232    /// 这个码的说明文字;[`DiagnosticCode::Other`] 没有固定文字。
233    pub fn message(self) -> Option<&'static str> {
234        DIAGNOSTIC_CODES
235            .iter()
236            .find(|(code, _)| *code == self)
237            .map(|(_, text)| *text)
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244
245    /// 编号一经发布就冻结,表按编号排、每条文字只出现一次,查表来回一致。
246    #[test]
247    fn diagnostic_codes_are_frozen_and_round_trip() {
248        for (index, (code, message)) in DIAGNOSTIC_CODES.iter().enumerate() {
249            assert_eq!(usize::from(code.value()), index + 1, "{message}");
250            assert_eq!(DiagnosticCode::from_message(message), *code);
251            assert_eq!(code.message(), Some(*message));
252        }
253        assert_eq!(DiagnosticCode::Other.value(), 0xFFFF);
254        assert_eq!(DiagnosticCode::Other.message(), None);
255        assert_eq!(
256            DiagnosticCode::from_message("database is locked"),
257            DiagnosticCode::Other
258        );
259        assert_eq!(
260            DiagnosticCode::from_message(NINE_KEY_DIGIT_LIMIT).value(),
261            16
262        );
263        assert_eq!(
264            DiagnosticCode::from_message(SUPER_JIANPIN_QUERY_FAILED).value(),
265            24
266        );
267        assert_eq!(
268            DiagnosticCode::from_message(LANGUAGE_DICTIONARY_UNAVAILABLE).value(),
269            25
270        );
271    }
272
273    /// 本文件里 `pub const` 字符串常量的名字和值,值可能换到下一行。
274    fn constants() -> Vec<(&'static str, &'static str)> {
275        let source = include_str!("diagnostics.rs");
276        let production = &source[..source.find("// ---- 诊断码").unwrap()];
277        production
278            .split("pub const ")
279            .skip(1)
280            .map(|item| {
281                let name = &item[..item.find(':').unwrap()];
282                let literal = &item[item.find('"').unwrap() + 1..];
283                (name, &literal[..literal.find('"').unwrap()])
284            })
285            .collect()
286    }
287
288    /// 「KeyResult diagnostics」和「Local mode query diagnostics」两段的每个常量都有码:新加的诊断忘了给码,这里会报出它的名字。
289    #[test]
290    fn every_key_result_diagnostic_constant_has_a_code() {
291        let source = include_str!("diagnostics.rs");
292        let start = source.find("// ---- KeyResult diagnostics").unwrap();
293        let end = source.find("// ---- Engine errors").unwrap();
294        let section = &source[start..end];
295        let mut checked = 0;
296        for (name, message) in constants() {
297            if section.contains(&format!("pub const {name}:")) {
298                assert_ne!(
299                    DiagnosticCode::from_message(message),
300                    DiagnosticCode::Other,
301                    "{name}"
302                );
303                checked += 1;
304            }
305        }
306        assert_eq!(checked, 24);
307    }
308
309    /// 直接交给 `KeyResult::with_diagnostic` 的固定常量也都有码,不管它定义在本文件哪一段:九键笔画筛选给的 `LANGUAGE_DICTIONARY_UNAVAILABLE` 在宿主门面错误那段,按分段数漏掉过它。扫的是引擎源码里每个 `with_diagnostic(...)` 的实参(测试模块除外)。
310    #[test]
311    fn every_constant_passed_to_with_diagnostic_has_a_code() {
312        fn sources(directory: &std::path::Path, found: &mut Vec<std::path::PathBuf>) {
313            for entry in std::fs::read_dir(directory).unwrap() {
314                let path = entry.unwrap().path();
315                if path.is_dir() {
316                    sources(&path, found);
317                } else if path.extension().is_some_and(|extension| extension == "rs") {
318                    found.push(path);
319                }
320            }
321        }
322        /// 从左括号之后到与它配对的右括号之前,跳过字符串字面量里的括号。
323        fn argument(text: &str) -> &str {
324            let (mut depth, mut quoted, mut escaped) = (1, false, false);
325            for (index, character) in text.char_indices() {
326                match character {
327                    _ if escaped => escaped = false,
328                    '\\' if quoted => escaped = true,
329                    '"' => quoted = !quoted,
330                    '(' if !quoted => depth += 1,
331                    ')' if !quoted => {
332                        depth -= 1;
333                        if depth == 0 {
334                            return &text[..index];
335                        }
336                    }
337                    _ => {}
338                }
339            }
340            panic!("unbalanced with_diagnostic call");
341        }
342
343        let constants = constants();
344        let mut files = Vec::new();
345        sources(
346            &std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"),
347            &mut files,
348        );
349        let mut passed = std::collections::BTreeSet::new();
350        for path in files {
351            let source = std::fs::read_to_string(&path).unwrap();
352            let production = source.split("\n#[cfg(test)]\nmod tests").next().unwrap();
353            for (start, call) in production.match_indices("with_diagnostic(") {
354                let argument = argument(&production[start + call.len()..]);
355                for reference in argument.split("diagnostics::").skip(1) {
356                    let name: String = reference
357                        .chars()
358                        .take_while(|character| {
359                            character.is_ascii_uppercase()
360                                || character.is_ascii_digit()
361                                || *character == '_'
362                        })
363                        .collect();
364                    if name.len() < 2 {
365                        continue;
366                    }
367                    let message = constants
368                        .iter()
369                        .find(|(constant, _)| *constant == name)
370                        .unwrap_or_else(|| panic!("{} uses unknown {name}", path.display()))
371                        .1;
372                    assert_ne!(
373                        DiagnosticCode::from_message(message),
374                        DiagnosticCode::Other,
375                        "{} passes {name} to with_diagnostic without a code",
376                        path.display()
377                    );
378                    passed.insert(name);
379                }
380            }
381        }
382        assert!(
383            passed.contains("LANGUAGE_DICTIONARY_UNAVAILABLE")
384                && passed.contains("NINE_KEY_DIGIT_LIMIT"),
385            "{passed:?}"
386        );
387    }
388}