Skip to main content

fandhe_ai/text/
error.rs

1//! 語彙 lookup 型のテキスト変換(`text` モジュール)の型付きエラー `TextError`。
2//!
3//! 役割: `text` 内の各段(上限検査・標準化・語彙構築・変換)が返す失敗を 1 つの
4//! enum に集約する。呼び出し元は `limits`(本イシュー)と、後続イシューの語彙・
5//! adapt・transform。設計の正は `docs/facade-text-vectorization-design.md` §6
6//! (variant 案)・§8(上限)。
7//!
8//! 契約(OWASP A09): **入力文字列・語彙の中身を一切持たない**。保持するのは位置
9//! (index)・長さ・上限値だけで、`Display`/`Debug` からも文字列が漏れない。
10//! `OnnxError`・`ModelError` と同型の自己完結型(内部クレートのエラー enum は
11//! 抱えない。例外は形状不整合を表す `ShapeError` のみ)。
12//!
13//! `fandhe_ai::text::TextError` として公開される(設計記録 §16.2・§16.3)。
14
15use std::fmt;
16
17use crate::ShapeError;
18
19/// テキスト変換の失敗を表す型付きエラー。`#[non_exhaustive]`: 後続段の variant
20/// 追加を非破壊にするため。
21///
22/// どの variant も入力文字列・語彙の中身を持たない(位置・長さ・件数・上限値だけ)。
23/// `Display`/`Debug` からも文字列は漏れない。
24#[non_exhaustive]
25#[derive(Debug)]
26pub enum TextError {
27    /// バッチ要素数(`transform`)またはコーパス件数(`adapt`)が上限超過。
28    BatchTooLarge { len: usize, max: usize },
29    /// `index` 番目の入力文字列のバイト数が上限超過。`Standardize::UnicodeLower` では
30    /// `len` は標準化後のバイト数(標準化で伸びた結果が上限を超えた場合)。
31    InputTooLong {
32        index: usize,
33        len: usize,
34        max: usize,
35    },
36    /// `adapt` のコーパス総バイト数が上限超過(加算 overflow 時は
37    /// `total_bytes = usize::MAX`)。
38    CorpusTooLarge { total_bytes: usize, max: usize },
39    /// 語彙数(予約 2 件を含む)が上限超過。
40    VocabularyTooLarge { len: usize, max: usize },
41    /// `index` 番目の語彙のバイト数が上限超過。
42    VocabularyTokenTooLong {
43        index: usize,
44        len: usize,
45        max: usize,
46    },
47    /// `index` 番目の語彙が空文字列。
48    EmptyVocabularyToken { index: usize },
49    /// `index` 番目の語彙が予約語(`""`・`"[UNK]"`)。
50    ReservedVocabularyToken { index: usize },
51    /// 語彙の `first` 番目と `second` 番目が重複。
52    DuplicateVocabularyToken { first: usize, second: usize },
53    /// `max_tokens` が 2 以下で語彙が入らない。
54    InvalidMaxTokens { max_tokens: usize },
55    /// n-gram の `n` が 0 または上限超過。
56    InvalidNgrams { n: usize, max: usize },
57    /// `output_sequence_length` が上限超過。
58    OutputSequenceLengthTooLarge { len: usize, max: usize },
59    /// `adapt` 中の異なり語数が上限超過。
60    TooManyDistinctTokens { max: usize },
61    /// 出力要素数 `B × L`(bag 系出力は `B × W`。またはそのバイト数)が上限超過・overflow
62    /// (overflow 時は `elements = usize::MAX`)。
63    OutputTooLarge { elements: usize, max: usize },
64    /// 上限の設定値が絶対上限を超えた(`TextLimits::with_*`)。`limit` は
65    /// フィールド名の固定文字列。設計記録 §6 の variant 案にない追加分
66    /// (§8「設定値が絶対上限を超えたら `Err`」を満たすため。公開時の承認対象)。
67    LimitAboveAbsoluteMaximum {
68        limit: &'static str,
69        value: usize,
70        max: usize,
71    },
72    /// n-gram の生成数の上限見積り `tokens × n` が `usize` を超えた。設計記録 §6 の
73    /// variant 案にない追加分(§8 の `checked_mul` 検査を満たすため。公開時の承認対象)。
74    NgramCountOverflow { tokens: usize, n: usize },
75    /// `adapt` で語の出現頻度(`u64`)が加算 overflow した。語の文字列は持たない。
76    /// 設計記録 §6 の variant 案にない追加分(§8 の `checked_add` 検査を満たすため。
77    /// 公開時の承認対象)。
78    FrequencyOverflow,
79    /// `transform_tf_idf` を、idf 重みを持たないインスタンス(`from_vocabulary`/`adapt`
80    /// で作ったもの)で呼んだ。`from_vocabulary_with_idf`/`adapt_tf_idf` で作る必要がある。
81    MissingIdfWeights,
82    /// `from_vocabulary_with_idf` の idf 重みの個数が語彙の個数と一致しない。
83    /// `expected` は語彙の個数(予約 2 件を含まない)。
84    IdfWeightsLengthMismatch { len: usize, expected: usize },
85    /// `index` 番目の idf 重みが有限でない、または負。値は持たない。
86    InvalidIdfWeight { index: usize },
87    /// `pad_to_max_tokens` が true なのに `max_tokens` が `None`(出力幅が決まらない)。
88    PadToMaxTokensWithoutMaxTokens,
89    /// `index` 番目の入力の(n-gram 込みの)トークン数 `count` が、f32 で整数を正確に
90    /// 数えられる上限 `max`(2^24)を超えた。件数の丸めを黙って許さないための fail-closed。
91    TokenCountNotExact {
92        index: usize,
93        count: usize,
94        max: usize,
95    },
96    /// `StringLookup` の逆引きで id が負または語彙数以上。`index` は入力 id の位置
97    /// (`token` は常に 0、`invert` は行優先で平坦化した最初の範囲外の位置)。id の値は持たない。
98    IdOutOfRange { index: usize },
99    /// テンソル形状の不整合。
100    Shape(ShapeError),
101}
102
103impl fmt::Display for TextError {
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        match self {
106            TextError::BatchTooLarge { len, max } => write!(
107                f,
108                "バッチ(コーパス)の件数が上限を超えています({len} 件 > 上限 {max} 件)"
109            ),
110            TextError::InputTooLong { index, len, max } => write!(
111                f,
112                "入力文字列が上限を超えています(index={index}・{len} バイト > 上限 {max} バイト)"
113            ),
114            TextError::CorpusTooLarge { total_bytes, max } => write!(
115                f,
116                "コーパス総バイト数が上限を超えています({total_bytes} バイト > 上限 {max} バイト)"
117            ),
118            TextError::VocabularyTooLarge { len, max } => {
119                write!(f, "語彙数が上限を超えています({len} 件 > 上限 {max} 件)")
120            }
121            TextError::VocabularyTokenTooLong { index, len, max } => write!(
122                f,
123                "語彙が上限を超えています(index={index}・{len} バイト > 上限 {max} バイト)"
124            ),
125            TextError::EmptyVocabularyToken { index } => {
126                write!(f, "語彙が空文字列です(index={index})")
127            }
128            TextError::ReservedVocabularyToken { index } => {
129                write!(f, "語彙が予約語です(index={index})")
130            }
131            TextError::DuplicateVocabularyToken { first, second } => {
132                write!(f, "語彙が重複しています(index={first} と index={second})")
133            }
134            TextError::InvalidMaxTokens { max_tokens } => {
135                write!(f, "max_tokens は 3 以上が必要です(指定値 {max_tokens})")
136            }
137            TextError::InvalidNgrams { n, max } => {
138                write!(f, "n-gram の n が不正です(n={n}・有効範囲 1..={max})")
139            }
140            TextError::OutputSequenceLengthTooLarge { len, max } => write!(
141                f,
142                "output_sequence_length が上限を超えています({len} > 上限 {max})"
143            ),
144            TextError::TooManyDistinctTokens { max } => {
145                write!(f, "異なり語数が上限 {max} 件を超えています")
146            }
147            TextError::OutputTooLarge { elements, max } => write!(
148                f,
149                "出力要素数が上限を超えています({elements} 要素 > 上限 {max} 要素)"
150            ),
151            TextError::LimitAboveAbsoluteMaximum { limit, value, max } => write!(
152                f,
153                "上限の設定値が絶対上限を超えています({limit}={value} > 絶対上限 {max})"
154            ),
155            TextError::NgramCountOverflow { tokens, n } => write!(
156                f,
157                "n-gram の生成数が数えられる範囲を超えています(トークン数 {tokens} × n={n})"
158            ),
159            TextError::FrequencyOverflow => {
160                write!(f, "語の出現頻度が数えられる範囲を超えています")
161            }
162            TextError::MissingIdfWeights => write!(
163                f,
164                "idf 重みがありません(from_vocabulary_with_idf または adapt_tf_idf で作る必要があります)"
165            ),
166            TextError::IdfWeightsLengthMismatch { len, expected } => write!(
167                f,
168                "idf 重みの個数が語彙の個数と一致しません({len} 個 != 語彙 {expected} 個)"
169            ),
170            TextError::InvalidIdfWeight { index } => {
171                write!(f, "idf 重みが有限の非負値ではありません(index={index})")
172            }
173            TextError::PadToMaxTokensWithoutMaxTokens => {
174                write!(f, "pad_to_max_tokens には max_tokens の指定が必要です")
175            }
176            TextError::TokenCountNotExact { index, count, max } => write!(
177                f,
178                "トークン数が f32 で正確に数えられる上限を超えています(index={index}・{count} 個 > 上限 {max} 個)"
179            ),
180            TextError::IdOutOfRange { index } => {
181                write!(f, "id が語彙の範囲外です(index={index})")
182            }
183            TextError::Shape(e) => write!(f, "テンソル形状が不正です: {e}"),
184        }
185    }
186}
187
188impl std::error::Error for TextError {
189    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
190        match self {
191            TextError::Shape(e) => Some(e),
192            _ => None,
193        }
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use std::error::Error;
201
202    fn all_variants() -> Vec<TextError> {
203        vec![
204            TextError::BatchTooLarge { len: 3, max: 2 },
205            TextError::InputTooLong {
206                index: 1,
207                len: 9,
208                max: 8,
209            },
210            TextError::CorpusTooLarge {
211                total_bytes: 9,
212                max: 8,
213            },
214            TextError::VocabularyTooLarge { len: 5, max: 4 },
215            TextError::VocabularyTokenTooLong {
216                index: 2,
217                len: 7,
218                max: 6,
219            },
220            TextError::EmptyVocabularyToken { index: 0 },
221            TextError::ReservedVocabularyToken { index: 1 },
222            TextError::DuplicateVocabularyToken {
223                first: 0,
224                second: 4,
225            },
226            TextError::InvalidMaxTokens { max_tokens: 2 },
227            TextError::InvalidNgrams { n: 0, max: 8 },
228            TextError::OutputSequenceLengthTooLarge { len: 11, max: 10 },
229            TextError::TooManyDistinctTokens { max: 10 },
230            TextError::OutputTooLarge {
231                elements: 11,
232                max: 10,
233            },
234            TextError::LimitAboveAbsoluteMaximum {
235                limit: "max_ngrams",
236                value: 9,
237                max: 8,
238            },
239            TextError::NgramCountOverflow {
240                tokens: usize::MAX,
241                n: 2,
242            },
243            TextError::FrequencyOverflow,
244            TextError::MissingIdfWeights,
245            TextError::IdfWeightsLengthMismatch {
246                len: 3,
247                expected: 4,
248            },
249            TextError::InvalidIdfWeight { index: 2 },
250            TextError::PadToMaxTokensWithoutMaxTokens,
251            TextError::TokenCountNotExact {
252                index: 1,
253                count: 9,
254                max: 8,
255            },
256            TextError::IdOutOfRange { index: 3 },
257            TextError::Shape(ShapeError::RankMismatch {
258                expected: 2,
259                actual: 3,
260            }),
261        ]
262    }
263
264    #[test]
265    fn display_is_nonempty_for_every_variant() {
266        for e in all_variants() {
267            assert!(!e.to_string().is_empty(), "{e:?}");
268        }
269    }
270
271    #[test]
272    fn source_is_some_only_for_shape() {
273        for e in all_variants() {
274            let is_shape = matches!(e, TextError::Shape(_));
275            assert_eq!(e.source().is_some(), is_shape, "{e:?}");
276        }
277    }
278
279    #[test]
280    fn variants_hold_numbers_only_so_no_input_text_leaks() {
281        // 構造上、文字列を持つのは固定の `limit` 名だけ。目印の文字列を入力に
282        // 見立てても Display/Debug に現れないことの回帰ガード。
283        let marker = "SECRET_MARKER_TOKEN";
284        for e in all_variants() {
285            assert!(!e.to_string().contains(marker));
286            assert!(!format!("{e:?}").contains(marker));
287        }
288    }
289}