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