Skip to main content

ling/lexer/
mod.rs

1// src/lexer/mod.rs — hand-written polyglot lexer
2mod cursor;
3mod token;
4mod unicode;
5
6pub use cursor::Cursor;
7pub use token::Token;
8
9pub struct Lexer<'a> {
10    source: &'a str,
11    pos: usize,
12}
13
14impl<'a> Lexer<'a> {
15    pub fn new(source: &'a str) -> Self {
16        Self { source, pos: 0 }
17    }
18
19    fn peek(&self) -> Option<char> {
20        self.source[self.pos..].chars().next()
21    }
22
23    fn peek_nth(&self, n: usize) -> Option<char> {
24        self.source[self.pos..].chars().nth(n)
25    }
26
27    fn advance(&mut self) -> Option<char> {
28        let ch = self.source[self.pos..].chars().next()?;
29        self.pos += ch.len_utf8();
30        Some(ch)
31    }
32
33    fn rest(&self) -> &str {
34        &self.source[self.pos..]
35    }
36
37    fn skip_whitespace_and_comments(&mut self) {
38        loop {
39            while matches!(
40                self.peek(),
41                Some(' ') | Some('\t') | Some('\n') | Some('\r') | Some('\x0C')
42            ) {
43                self.advance();
44            }
45            if self.peek() == Some('/') && self.peek_nth(1) == Some('/') {
46                while self.peek().is_some_and(|c| c != '\n') {
47                    self.advance();
48                }
49                continue;
50            }
51            // Shebang or # comment
52            if self.peek() == Some('#') {
53                while self.peek().is_some_and(|c| c != '\n') {
54                    self.advance();
55                }
56                continue;
57            }
58            break;
59        }
60    }
61
62    fn lex_string(&mut self) -> Token {
63        self.advance(); // opening "
64        let mut s = std::string::String::new();
65        loop {
66            match self.advance() {
67                None => break,
68                Some('"') => break,
69                Some('\\') => match self.advance() {
70                    Some('n') => s.push('\n'),
71                    Some('t') => s.push('\t'),
72                    Some('r') => s.push('\r'),
73                    Some('"') => s.push('"'),
74                    Some('\\') => s.push('\\'),
75                    Some('0') => s.push('\0'),
76                    Some(c) => {
77                        s.push('\\');
78                        s.push(c);
79                    },
80                    None => break,
81                },
82                Some(c) => s.push(c),
83            }
84        }
85        Token::String(s)
86    }
87
88    fn lex_number(&mut self) -> Token {
89        let start = self.pos;
90        let mut has_dot = false;
91        loop {
92            match self.peek() {
93                Some('0'..='9') => {
94                    self.advance();
95                },
96                Some('.') if !has_dot && matches!(self.peek_nth(1), Some('0'..='9')) => {
97                    has_dot = true;
98                    self.advance();
99                },
100                _ => break,
101            }
102        }
103        Token::Number(self.source[start..self.pos].to_string())
104    }
105
106    fn lex_word(&mut self) -> Token {
107        let start = self.pos;
108        // Scan contiguous word characters: letters, digits, underscore, and Unicode
109        // combining marks (tone marks, vowel signs, diacritics) that Rust's
110        // is_alphabetic() misses because they lack the Other_Alphabetic property.
111        while let Some(c) = self.peek() {
112            if c.is_alphanumeric() || c == '_' || is_unicode_combining(c) {
113                self.advance();
114            } else {
115                break;
116            }
117        }
118        let word = &self.source[start..self.pos];
119        Self::classify_word(word)
120    }
121
122    /// Map a word (ASCII or Unicode) to its canonical token.
123    /// Chinese/Japanese/Korean/Russian keywords resolve here so there is
124    /// no pre-processing that could corrupt variable names or string contents.
125    fn classify_word(word: &str) -> Token {
126        match word {
127            // ── English keywords ────────────────────────────────────────────
128            "bind" | "令" | "灵符" => Token::Bind,
129            "do" | "执" => Token::Do,
130            "fn" | "函" => Token::Fn,
131            "mod" | "核" => Token::Mod,
132            "type" | "符" | "型" | "타입" | "ชนิด" => Token::Type,
133            // Module imports: use / 载 (zh) / 使う (ja) / 사용 (ko) / ใช้ / นำเข้า (th)
134            "use" | "载" | "引" | "사용" | "使う" | "ใช้" | "นำเข้า" | "importar" | "nutzen"
135            | "utiliser" => Token::Use,
136            "if" | "若" | "如" => Token::If,
137            "else" | "否则" | "否" => Token::Else,
138            "while" | "循" | "当" => Token::While,
139            "for" | "历" => Token::For,
140            "in" | "于" => Token::In,
141            "match" | "配" => Token::Match,
142            "return" | "归" => Token::Return,
143            "own" | "拥有" | "独" | "所有" | "소유" | "เป็นเจ้าของ" => Token::Own,
144            "lend" | "借" | "貸す" | "빌려" | "ให้ยืม" => Token::Lend,
145            "share" | "共享" | "共" | "共有" | "공유" | "แบ่งปัน" => Token::Share,
146            "move" | "移动" | "移" | "移動" | "이동" | "ย้าย" => Token::Move,
147            "copy" | "复制" | "复" | "複製" | "복사" | "คัดลอก" => Token::Copy,
148            "async" | "异步" | "异" => Token::Async,
149            "wait" | "等待" | "待" => Token::Wait,
150            "as" | "为" | "として" | "로서" | "เป็น" => Token::As,
151            "where" | "条件" | "但し" | "단" | "โดยที่" => Token::Where,
152            "post" | "发布" | "出" | "投稿" | "게시" | "ส่ง" => Token::Post,
153            "give" | "给" | "予" | "渡す" | "전달" | "ให้" => Token::Give,
154            "fit" | "适合" | "適合" | "적합" | "เหมาะสม" => Token::Fit,
155            // `form` — record/struct definition (EN · ZH · JA · KO · TH)
156            "form" | "形式" | "形" | "構造" | "구조" | "โครงสร้าง" => {
157                Token::Form
158            },
159            // `choose` — sum type / enum definition (EN · ZH · JA · KO · TH)
160            "choose" | "选择" | "选" | "選択" | "선택" | "เลือกแบบ" => {
161                Token::Choose
162            },
163            "can" | "能" | "できる" | "가능" | "สามารถ" => Token::Can,
164            "change" | "改变" | "变" | "変える" | "변경" | "เปลี่ยนแปลง" => Token::Change,
165            // stop/again/try/spawn's ja/ko forms live in the Japanese/Korean
166            // sections below — only th was missing here.
167            "stop" | "停止" | "止" | "หยุด" => Token::Stop,
168            "again" | "继续" | "ทำอีก" => Token::Again,
169            "try" | "尝试" | "试" | "ลอง" => Token::Try,
170            "sure" | "确定" | "确" | "確か" | "확실" | "แน่นอน" => Token::Sure,
171            "maybe" | "可能" | "或" | "多分" | "아마도" | "อาจจะ" => Token::Maybe,
172            "pure" | "纯" | "純粋" | "순수" | "บริสุทธิ์" => Token::Pure,
173            "spawn" | "生成" | "启" | "สร้าง" => Token::Spawn,
174            "asm" => Token::Asm,
175            "ok" | "好" | "可" | "良い" | "좋아" | "ตกลง" => Token::Ok,
176            "bad" | "坏" | "误" | "悪い" | "나쁨" | "ผิดพลาด" => Token::Bad,
177            "none" | "无" | "なし" | "없음" | "ไม่มี" => Token::None,
178            // Japanese — full keyword set with short kanji shorthands
179            "束縛" | "バ" => Token::Bind,
180            "実行" | "執" => Token::Do,
181            "関数" | "関" => Token::Fn,
182            "モジュール" | "模" => Token::Mod,
183            "もし" => Token::If,
184            "他" => Token::Else,
185            "間" | "一方" => Token::While,
186            "繰" | "ために" => Token::For,
187            "の中" => Token::In,
188            "一致" => Token::Match,
189            "戻る" | "帰る" => Token::Return,
190            "試す" => Token::Try,
191            "待つ" => Token::Wait,
192            "非同期" => Token::Async,
193            "起動" => Token::Spawn,
194            "止まれ" | "停め" => Token::Stop,
195            "継続" => Token::Again,
196            // Booleans — shared across Chinese, Japanese, Korean, Thai (already in Thai section)
197            "true" | "真" => Token::Bool(true),
198            "false" | "假" | "偽" => Token::Bool(false),
199            // Korean — full keyword set with short Hangul forms
200            "바인드" | "묶" => Token::Bind,
201            "실행" => Token::Do,
202            "함수" => Token::Fn,
203            "모듈" => Token::Mod,
204            "만약" | "조건" => Token::If,
205            "아니면" => Token::Else,
206            "동안" | "반복" => Token::While,
207            "위해" => Token::For,
208            "안에" => Token::In,
209            "매치" => Token::Match,
210            "반환" | "귀환" => Token::Return,
211            "시도" => Token::Try,
212            "기다려" => Token::Wait,
213            "비동기" => Token::Async,
214            "생성" => Token::Spawn,
215            "멈춤" => Token::Stop,
216            "계속" => Token::Again,
217            "참" => Token::Bool(true),
218            "거짓" => Token::Bool(false),
219            // Russian
220            "связать" => Token::Bind,
221            "сделать" => Token::Do,
222            "если" => Token::If,
223            "иначе" => Token::Else,
224            "пока" => Token::While,
225            "для" => Token::For,
226            "вернуть" => Token::Return,
227            // Thai
228            "ผูก" => Token::Bind,
229            "ทำ" => Token::Do,
230            "ฟังก์ชัน" => Token::Fn,
231            "โมดูล" => Token::Mod,
232            "ถ้า" => Token::If,
233            "มิฉะนั้น" => Token::Else,
234            "ขณะที่" => Token::While,
235            "สำหรับ" => Token::For,
236            "ใน" => Token::In,
237            "จับคู่" => Token::Match,
238            "คืน" => Token::Return,
239            "รอ" => Token::Wait,
240            "ไม่พร้อมกัน" => Token::Async,
241            "จริง" => Token::Bool(true),
242            "เท็จ" => Token::Bool(false),
243            // Hindi
244            "बाँधो" => Token::Bind,
245            "करो" => Token::Do,
246            "अगर" => Token::If,
247            "नहींतो" => Token::Else,
248            "जबकि" => Token::While,
249            "केलिए" => Token::For,
250            "वापस" => Token::Return,
251            "सत्य" => Token::Bool(true),
252            "असत्य" => Token::Bool(false),
253            // Arabic
254            "ربط" => Token::Bind,
255            "افعل" => Token::Do,
256            "إذا" => Token::If,
257            "وإلا" => Token::Else,
258            "بينما" => Token::While,
259            "لأجل" => Token::For,
260            "في" => Token::In,
261            "أعد" => Token::Return,
262            "صحيح" => Token::Bool(true),
263            "خطأ" => Token::Bool(false),
264            // Spanish
265            "enlazar" => Token::Bind,
266            "hacer" => Token::Do,
267            "si" => Token::If,
268            "sino" => Token::Else,
269            "mientras" => Token::While,
270            "para" => Token::For,
271            "retornar" => Token::Return,
272            "verdadero" => Token::Bool(true),
273            "falso" => Token::Bool(false),
274            // French
275            "lier" => Token::Bind,
276            "faire" => Token::Do,
277            "func" => Token::Fn,
278            "module" => Token::Mod,
279            "sinon" => Token::Else,
280            "tantque" => Token::While,
281            "retourner" => Token::Return,
282            "vrai" => Token::Bool(true),
283            "faux" => Token::Bool(false),
284            // German
285            "binden" => Token::Bind,
286            "machen" => Token::Do,
287            "wenn" => Token::If,
288            "sonst" => Token::Else,
289            "solange" => Token::While,
290            "für" => Token::For,
291            "zurück" => Token::Return,
292            "wahr" => Token::Bool(true),
293            "falsch" => Token::Bool(false),
294            // Portuguese (deduplicated from Spanish)
295            "ligar" => Token::Bind,
296            "fazer" => Token::Do,
297            "se" => Token::If,
298            "senão" => Token::Else,
299            "enquanto" => Token::While,
300            "verdadeiro" => Token::Bool(true),
301            // Everything else is an identifier
302            other => Token::Ident(other.to_string()),
303        }
304    }
305
306    pub fn next_token(&mut self) -> Option<Token> {
307        self.skip_whitespace_and_comments();
308        let ch = self.peek()?;
309
310        // String literal
311        if ch == '"' {
312            return Some(self.lex_string());
313        }
314
315        // Number literal
316        if ch.is_ascii_digit() {
317            return Some(self.lex_number());
318        }
319
320        // Word (keyword or identifier) — handles ASCII and all Unicode letters/ideographs
321        if ch.is_alphabetic() || ch == '_' || is_unicode_combining(ch) {
322            return Some(self.lex_word());
323        }
324
325        // Multi-character punctuation / operators
326        let rest = self.rest();
327
328        // `..`
329        if rest.starts_with("..") {
330            self.advance();
331            self.advance();
332            return Some(Token::DotDot);
333        }
334        // `::`
335        if rest.starts_with("::") {
336            self.advance();
337            self.advance();
338            return Some(Token::ColonColon);
339        }
340        // `==`
341        if rest.starts_with("==") {
342            self.advance();
343            self.advance();
344            return Some(Token::EqEq);
345        }
346        // `!=`
347        if rest.starts_with("!=") {
348            self.advance();
349            self.advance();
350            return Some(Token::Ne);
351        }
352        // `<=`
353        if rest.starts_with("<=") {
354            self.advance();
355            self.advance();
356            return Some(Token::Le);
357        }
358        // `>=`
359        if rest.starts_with(">=") {
360            self.advance();
361            self.advance();
362            return Some(Token::Ge);
363        }
364        // `->`
365        if rest.starts_with("->") {
366            self.advance();
367            self.advance();
368            return Some(Token::Arrow);
369        }
370        // `=>`
371        if rest.starts_with("=>") {
372            self.advance();
373            self.advance();
374            return Some(Token::FatArrow);
375        }
376        // `&&`
377        if rest.starts_with("&&") {
378            self.advance();
379            self.advance();
380            return Some(Token::And);
381        }
382        // `||`
383        if rest.starts_with("||") {
384            self.advance();
385            self.advance();
386            return Some(Token::Or);
387        }
388
389        // Single-character tokens
390        self.advance();
391        Some(match ch {
392            '=' => Token::Eq,
393            '<' => Token::Lt,
394            '>' => Token::Gt,
395            '!' => Token::Not,
396            '+' => Token::Plus,
397            '-' => Token::Minus,
398            '*' => Token::Star,
399            '/' => Token::Slash,
400            '%' => Token::Percent,
401            '.' => Token::Dot,
402            '&' => Token::Ampersand,
403            '(' => Token::LParen,
404            ')' => Token::RParen,
405            '{' => Token::LBrace,
406            '}' => Token::RBrace,
407            '[' => Token::LBracket,
408            ']' => Token::RBracket,
409            ',' => Token::Comma,
410            ':' => Token::Colon,
411            ';' => Token::Semicolon,
412            '|' => Token::Or, // single | treated as Or (for closure context)
413            c => Token::Error(c.to_string()),
414        })
415    }
416}
417
418/// Returns true for Unicode combining marks (Mn/Mc category) that Rust's
419/// `is_alphabetic()` may miss — Thai tone marks, Devanagari vowel signs,
420/// Arabic diacritics, Hebrew cantillation, etc.
421fn is_unicode_combining(c: char) -> bool {
422    let cp = c as u32;
423    matches!(cp,
424        0x0300..=0x036F |   // Combining Diacritical Marks (Latin)
425        0x0483..=0x0489 |   // Combining Cyrillic
426        0x0591..=0x05C7 |   // Hebrew cantillation / points
427        0x0610..=0x061A |   // Arabic extended
428        0x064B..=0x065F |   // Arabic diacritics (harakat)
429        0x0670         |    // Arabic superscript alef
430        0x06D6..=0x06DC |   // Arabic small high letters
431        0x0730..=0x074A |   // Syriac diacritics
432        0x0816..=0x082D |   // Samaritan diacritics
433        0x0900..=0x0903 |   // Devanagari (anusvara, visarga)
434        0x093A..=0x094F |   // Devanagari vowel signs / halant
435        0x0951..=0x0957 |   // Devanagari stress marks
436        0x0962..=0x0963 |   // Devanagari vowel signs
437        0x0981..=0x0983 |   // Bengali
438        0x09BC         |    // Bengali nukta
439        0x09BE..=0x09C4 |   // Bengali vowel signs
440        0x09C7..=0x09C8 |   // Bengali vowel signs
441        0x09CB..=0x09CD |   // Bengali
442        0x0A01..=0x0A03 |   // Gurmukhi
443        0x0A3C         |    // Gurmukhi nukta
444        0x0A3E..=0x0A42 |   // Gurmukhi vowels
445        0x0B01..=0x0B03 |   // Oriya
446        0x0B3C..=0x0B4D |   // Oriya
447        0x0C00..=0x0C03 |   // Telugu
448        0x0C3E..=0x0C56 |   // Telugu vowels
449        0x0D00..=0x0D03 |   // Malayalam
450        0x0D3B..=0x0D4D |   // Malayalam vowels / chandrakkala
451        0x0E31         |    // Thai SARA AM
452        0x0E34..=0x0E3A |   // Thai vowel signs
453        0x0E47..=0x0E4E |   // Thai tone marks & other signs
454        0x0EB1         |    // Lao vowel sign
455        0x0EB4..=0x0EBC |   // Lao vowel signs
456        0x0EC8..=0x0ECD |   // Lao tone marks
457        0x3099..=0x309A |   // Japanese combining dakuten
458        0xFE20..=0xFE2F     // Combining Half Marks
459    )
460}