Skip to main content

kashida/
pattern.rs

1//! The pattern language: IR types and the compiler.
2
3use crate::error::{CompileError, CompileErrorKind};
4use crate::grapheme::{is_joining_type, KASHIDA};
5use crate::rasm::resolve_group_name;
6use icu_properties::props::{JoiningGroup, JoiningType};
7use icu_properties::CodePointMapData;
8
9#[derive(Clone, Debug)]
10pub(crate) enum Token {
11    Group(JoiningGroup),      // @Name, positional rasm by group
12    ExactGroup(JoiningGroup), // =Name, that Joining_Group alone, no folding
13    GroupSet(Vec<Token>),     // {…}, any of its members
14    NotGroupSet(Vec<Token>),  // ^{…}, none of its members
15    Literal(u32),             // a letter, exact codepoint
16    Any,                      // * wildcard
17}
18
19// `base` at the guard's floor length, stepping down 1 per extra letter to
20// `min`, then holding (`min == base` is constant).
21#[derive(Clone, Copy, Debug)]
22pub(crate) enum Weight {
23    Priority { base: u8, min: u8 },
24    Suppress,
25}
26
27#[derive(Clone, Copy, Debug)]
28pub(crate) enum LengthGuard {
29    Exact(usize),
30    Min(usize),
31    Range { lo: usize, hi: usize },
32    // Matches a run of any length. The number is the run length where the
33    // priority is highest.
34    Open(usize),
35}
36
37#[derive(Clone, Debug)]
38pub(crate) struct CompiledPattern {
39    pub(crate) guard: Option<LengthGuard>,
40    pub(crate) tokens: Vec<Token>,
41    // weights[k] is the contribution at the gap before token k; index
42    // tokens.len() is the gap after the last token.
43    pub(crate) weights: Vec<Option<Weight>>,
44    pub(crate) leading_boundary: bool,
45    pub(crate) trailing_boundary: bool,
46}
47
48/// A compiled set of kashida insertion patterns.
49#[derive(Clone, Debug)]
50pub struct PatternSet {
51    pub(crate) patterns: Vec<CompiledPattern>,
52}
53
54fn strip_comment(raw: &str) -> String {
55    let body = match raw.find('#') {
56        Some(hash) => &raw[..hash],
57        None => raw,
58    };
59    body.trim().to_string()
60}
61
62// letter ::= a codepoint with a joining Joining_Type
63fn is_letter(ch: char) -> bool {
64    is_joining_type(CodePointMapData::<JoiningType>::new().get(ch))
65}
66
67// An `@` reference folds positionally through the rasm classes. A group in
68// none of them just matches itself alone. An `=` reference matches its
69// Joining_Group alone in any position. Under either prefix, `Tatweel` names
70// U+0640 ARABIC TATWEEL itself.
71fn resolve_reference(name: &str) -> Result<Token, CompileErrorKind> {
72    if name.strip_prefix(['@', '=']) == Some("Tatweel") {
73        return Ok(Token::Literal(KASHIDA as u32));
74    }
75    let group = resolve_group_name(name)?;
76    if name.starts_with('@') {
77        Ok(Token::Group(group))
78    } else {
79        Ok(Token::ExactGroup(group))
80    }
81}
82
83fn set_weight(
84    weights: &mut Vec<Option<Weight>>,
85    k: usize,
86    weight: Weight,
87) -> Result<(), CompileErrorKind> {
88    if k >= weights.len() {
89        weights.resize(k + 1, None);
90    }
91    // Two weights in one gap make no sense.
92    if weights[k].is_some() {
93        return Err(CompileErrorKind::ConflictingWeights);
94    }
95    weights[k] = Some(weight);
96    Ok(())
97}
98
99// A recursive-descent parser over one comment-stripped line:
100//
101//   line      ::= (use | pattern)? comment?
102//   use       ::= "use" set_name
103//   pattern   ::= guard? element+
104//   guard     ::= "[" bound ("+" | "-" bound)? "]"
105//   element   ::= token | weight | "."
106//   token     ::= reference | set | "^" (set | reference) | letter | "*"
107//   set       ::= "{" member+ "}"
108//   member    ::= reference | letter
109//   reference ::= ("@" | "=") name
110//   weight    ::= digit ("\" digit)? | "!"
111struct Parser<'a> {
112    chars: &'a [char],
113    pos: usize,
114}
115
116impl Parser<'_> {
117    fn peek(&self) -> Option<char> {
118        self.chars.get(self.pos).copied()
119    }
120
121    fn eat(&mut self, expected: char) -> bool {
122        if self.peek() == Some(expected) {
123            self.pos += 1;
124            true
125        } else {
126            false
127        }
128    }
129
130    fn digit(&mut self) -> Option<u8> {
131        let digit = self.peek()?.to_digit(10)?;
132        self.pos += 1;
133        Some(digit as u8)
134    }
135
136    fn skip_whitespace(&mut self) {
137        while matches!(self.peek(), Some(' ' | '\t')) {
138            self.pos += 1;
139        }
140    }
141
142    // pattern ::= guard? element+
143    fn pattern(&mut self) -> Result<CompiledPattern, CompileErrorKind> {
144        let guard = if self.peek() == Some('[') {
145            Some(self.guard()?)
146        } else {
147            None
148        };
149
150        let mut tokens: Vec<Token> = Vec::new();
151        let mut weights: Vec<Option<Weight>> = Vec::new();
152        let mut leading_boundary = false;
153        let mut trailing_boundary = false;
154
155        // element ::= token | weight | "."
156        loop {
157            self.skip_whitespace();
158            let Some(ch) = self.peek() else { break };
159            if ch == '.' {
160                self.pos += 1;
161                if tokens.is_empty() {
162                    leading_boundary = true;
163                } else {
164                    trailing_boundary = true;
165                }
166                continue;
167            }
168            if trailing_boundary {
169                return Err(CompileErrorKind::TokenAfterTrailingBoundary);
170            }
171            if ch.is_ascii_digit() || ch == '!' || ch == '\\' {
172                set_weight(&mut weights, tokens.len(), self.weight()?)?;
173                continue;
174            }
175            tokens.push(self.token(ch)?);
176        }
177
178        if tokens.is_empty() {
179            return Err(CompileErrorKind::NoLetters);
180        }
181        weights.resize(tokens.len() + 1, None);
182        // A weight in the gap between a token and a `.` can never land on a
183        // connection: no connection exists at a run's edge.
184        if (leading_boundary && weights[0].is_some())
185            || (trailing_boundary && weights[tokens.len()].is_some())
186        {
187            return Err(CompileErrorKind::WeightOutsideRun);
188        }
189        Ok(CompiledPattern {
190            guard,
191            tokens,
192            weights,
193            leading_boundary,
194            trailing_boundary,
195        })
196    }
197
198    // guard ::= "[" (bound | bound ":" | bound ":" bound | ":" bound ":") "]"
199    fn guard(&mut self) -> Result<LengthGuard, CompileErrorKind> {
200        self.pos += 1; // the `[`
201        let start = self.pos;
202        while self.peek().is_some_and(|c| c != ']') {
203            self.pos += 1;
204        }
205        if !self.eat(']') {
206            return Err(CompileErrorKind::UnterminatedLengthGuard);
207        }
208        let body: String = self.chars[start..self.pos - 1].iter().collect();
209        let trimmed = body.trim();
210        let invalid = || CompileErrorKind::InvalidLengthGuard(body.clone());
211        // bound ::= digit+.
212        let bound = |s: &str| -> Result<usize, CompileErrorKind> {
213            if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
214                return Err(invalid());
215            }
216            s.parse::<usize>().map_err(|_| invalid())
217        };
218        let guard = if let Some(stripped) = trimmed
219            .strip_prefix(':')
220            .and_then(|rest| rest.strip_suffix(':'))
221        {
222            LengthGuard::Open(bound(stripped)?)
223        } else if let Some(stripped) = trimmed.strip_suffix(':') {
224            LengthGuard::Min(bound(stripped)?)
225        } else if let Some(colon) = trimmed.find(':') {
226            LengthGuard::Range {
227                lo: bound(&trimmed[..colon])?,
228                hi: bound(&trimmed[colon + 1..])?,
229            }
230        } else {
231            LengthGuard::Exact(bound(trimmed)?)
232        };
233        // Reject guards no run can satisfy: a connection needs two letters, and
234        // a range must not be empty.
235        let bounds_ok = match guard {
236            LengthGuard::Exact(n) | LengthGuard::Min(n) | LengthGuard::Open(n) => n >= 2,
237            LengthGuard::Range { lo, hi } => lo >= 2 && lo <= hi,
238        };
239        if bounds_ok {
240            Ok(guard)
241        } else {
242            Err(invalid())
243        }
244    }
245
246    // weight ::= digit ("\" digit)? | "!"
247    fn weight(&mut self) -> Result<Weight, CompileErrorKind> {
248        if self.eat('!') {
249            return Ok(Weight::Suppress);
250        }
251        let Some(base) = self.digit() else {
252            // Only a lone `\` lands here.
253            return Err(CompileErrorKind::BackslashWithoutDigit);
254        };
255        let mut min = base;
256        if self.eat('\\') {
257            // The priority drops from the first digit down to the second as
258            // the run grows.
259            match self.digit() {
260                Some(end) => {
261                    min = end;
262                    if min > base {
263                        return Err(CompileErrorKind::IncreasingPriority { base, min });
264                    }
265                }
266                None => return Err(CompileErrorKind::ExpectedDigitAfterBackslash),
267            }
268        }
269        Ok(Weight::Priority { base, min })
270    }
271
272    // token ::= reference | set | "^" (set | reference) | letter | "*"
273    fn token(&mut self, ch: char) -> Result<Token, CompileErrorKind> {
274        match ch {
275            '*' => {
276                self.pos += 1;
277                Ok(Token::Any)
278            }
279            '{' => Ok(Token::GroupSet(self.set()?)),
280            '^' => {
281                self.pos += 1;
282                match self.peek() {
283                    Some('{') => Ok(Token::NotGroupSet(self.set()?)),
284                    Some('@' | '=') => Ok(Token::NotGroupSet(vec![self.reference()?])),
285                    _ => Err(CompileErrorKind::CaretNotFollowed),
286                }
287            }
288            '@' | '=' => self.reference(),
289            _ => {
290                self.pos += 1;
291                // A letter stands for itself alone. Anything else is an error,
292                // not a token.
293                if is_letter(ch) {
294                    Ok(Token::Literal(ch as u32))
295                } else {
296                    Err(CompileErrorKind::StrayCharacter(ch))
297                }
298            }
299        }
300    }
301
302    // set ::= "{" member+ "}"
303    // member ::= reference | letter
304    fn set(&mut self) -> Result<Vec<Token>, CompileErrorKind> {
305        self.pos += 1; // the `{`
306        let start = self.pos;
307        while self.peek().is_some_and(|c| c != '}') {
308            self.pos += 1;
309        }
310        if !self.eat('}') {
311            return Err(CompileErrorKind::UnterminatedGroupSet);
312        }
313        let body: String = self.chars[start..self.pos - 1].iter().collect();
314        if body.trim().is_empty() {
315            return Err(CompileErrorKind::EmptyGroupSet);
316        }
317        let mut members = Vec::new();
318        for part in body.split_whitespace() {
319            if part.starts_with('@') || part.starts_with('=') {
320                members.push(resolve_reference(part)?);
321            } else {
322                for ch in part.chars() {
323                    if is_letter(ch) {
324                        members.push(Token::Literal(ch as u32));
325                    } else {
326                        return Err(CompileErrorKind::StrayCharacter(ch));
327                    }
328                }
329            }
330        }
331        Ok(members)
332    }
333
334    // reference ::= ("@" | "=") name
335    // name ::= (ALPHA | "_")+
336    fn reference(&mut self) -> Result<Token, CompileErrorKind> {
337        let mut name = String::from(self.chars[self.pos]); // the `@` or `=`
338        self.pos += 1;
339        while let Some(c) = self.peek() {
340            if c.is_ascii_alphabetic() || c == '_' {
341                name.push(c);
342                self.pos += 1;
343            } else {
344                break;
345            }
346        }
347        if name.len() == 1 {
348            return Err(CompileErrorKind::EmptyGroupName);
349        }
350        resolve_reference(&name)
351    }
352}
353
354// line ::= pattern? comment?
355fn parse_line(raw: &str) -> Result<Option<CompiledPattern>, CompileErrorKind> {
356    let line = strip_comment(raw);
357    if line.is_empty() {
358        return Ok(None);
359    }
360    let chars: Vec<char> = line.chars().collect();
361    Parser {
362        chars: &chars,
363        pos: 0,
364    }
365    .pattern()
366    .map(Some)
367}
368
369// use ::= "use" set_name
370// for the rules after it to override.
371fn parse_use(line: &str) -> Option<&str> {
372    let rest = line.strip_prefix("use")?;
373    if rest.starts_with([' ', '\t']) {
374        Some(rest.trim())
375    } else {
376        None
377    }
378}
379
380/// Compiles pattern text into a [`PatternSet`].
381pub fn compile_pattern_text(text: &str) -> Result<PatternSet, CompileError> {
382    let mut patterns = Vec::new();
383    for (index, raw) in text.split('\n').enumerate() {
384        let raw = raw.strip_suffix('\r').unwrap_or(raw);
385        let context = |kind| CompileError {
386            kind,
387            line_number: index + 1,
388        };
389        if let Some(name) = parse_use(&strip_comment(raw)) {
390            let imported = crate::builtin::builtin_pattern_set(name)
391                .ok_or_else(|| context(CompileErrorKind::UnknownImport(name.to_string())))?;
392            patterns.extend(imported.patterns.iter().cloned());
393            continue;
394        }
395        if let Some(pattern) = parse_line(raw).map_err(context)? {
396            patterns.push(pattern);
397        }
398    }
399    Ok(PatternSet { patterns })
400}