Skip to main content

geulbus_core/
expr.rs

1//! 날개셋 값-식(value expression) 언어: 렉서 + 우선순위 파서 + 평가기.
2//!
3//! KeyTable 의 `value`(예: `T ? H3|_J : 0x23`, `119^(P&1)<<5`, `C0|0x82`)와 같은
4//! C 연산자 문법의 정수 식을 다룬다. 태그 `H3|`(한글 낱자)·`C0|`(제어 명령)는 식
5//! 안의 일급 값으로 취급한다. 참고: `research/01-nalgaeset-format.md` §1.
6
7use crate::unit::{self, Unit};
8use thiserror::Error;
9
10#[derive(Debug, Error, PartialEq, Eq)]
11pub enum ExprError {
12    #[error("예기치 못한 문자 {0:?} (위치 {1})")]
13    UnexpectedChar(char, usize),
14    #[error("예기치 못한 토큰: {0}")]
15    UnexpectedToken(String),
16    #[error("식이 끝나지 않음")]
17    UnexpectedEnd,
18    #[error("알 수 없는 변수 {0:?}")]
19    UnknownVar(String),
20    #[error("해석할 수 없는 낱자 operand {0:?}")]
21    BadUnit(String),
22    #[error("C0| operand 는 숫자여야 함: {0:?}")]
23    BadCommand(String),
24    #[error("정수가 아닌 값에 산술/비교 연산을 적용")]
25    NotInt,
26}
27
28/// 평가 문맥의 변수들. KeyTable 평가에는 T(오토마타 상태)와 P(수식어 비트마스크)가 쓰인다.
29/// 오토마타 식 평가에는 A~F(입력/조합 중 한글의 초·중·종성 서열번호), O, T 가 쓰인다.
30/// (오토마타 변수 의미는 `research/ngs-automata-help.txt` 참고.)
31#[derive(Clone, Copy, Debug, Default)]
32pub struct Ctx {
33    /// 오토마타 상태 id. 0 = 한글 조합 중이 아님. (`T`)
34    pub t: i64,
35    /// 수식어 비트마스크. bit0 = Shift. (`P`)
36    pub p: i64,
37    /// `A` = 입력 글쇠의 초성 서열(없으면 0; 갈마들이 토글이면 500).
38    pub a: i64,
39    /// `B` = 입력 글쇠의 중성 서열.
40    pub b: i64,
41    /// `C` = 입력 글쇠의 종성 서열.
42    pub c: i64,
43    /// `D` = 조합 중 한글의 초성 서열.
44    pub d: i64,
45    /// `E` = 조합 중 한글의 중성 서열.
46    pub e: i64,
47    /// `F` = 조합 중 한글의 종성 서열.
48    pub f: i64,
49    /// `O` = 부가 비트(1 입력 두벌식 + 2 조합중 두벌식 + 4 인위생성). 세벌식이면 0.
50    pub o: i64,
51}
52
53impl Ctx {
54    fn var(&self, name: &str) -> Option<i64> {
55        Some(match name {
56            "T" => self.t,
57            "P" => self.p,
58            "A" => self.a,
59            "B" => self.b,
60            "C" => self.c,
61            "D" => self.d,
62            "E" => self.e,
63            "F" => self.f,
64            "O" => self.o,
65            _ => return None,
66        })
67    }
68}
69
70/// 식 평가 결과. 정수, 한글 낱자(H3|), 제어 명령(C0|) 중 하나.
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72pub enum Value {
73    Int(i64),
74    Unit(Unit),
75    Command(u32),
76}
77
78impl Value {
79    fn truthy(self) -> bool {
80        match self {
81            Value::Int(n) => n != 0,
82            // 낱자/명령은 "존재" → 참 (실제 설정에서 조건부엔 정수만 옴)
83            _ => true,
84        }
85    }
86    fn as_int(self) -> Result<i64, ExprError> {
87        match self {
88            Value::Int(n) => Ok(n),
89            _ => Err(ExprError::NotInt),
90        }
91    }
92}
93
94// ── AST ─────────────────────────────────────────────────────────────────────
95
96#[derive(Clone, Debug, PartialEq, Eq)]
97pub enum Expr {
98    Int(i64),
99    Var(String),
100    Unit(Unit),
101    Command(u32),
102    Unary(UnOp, Box<Expr>),
103    Binary(BinOp, Box<Expr>, Box<Expr>),
104    Ternary(Box<Expr>, Box<Expr>, Box<Expr>),
105}
106
107#[derive(Clone, Copy, Debug, PartialEq, Eq)]
108pub enum UnOp {
109    Not,
110    Neg,
111    BitNot,
112}
113
114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
115pub enum BinOp {
116    Or,
117    And,
118    BitOr,
119    BitXor,
120    BitAnd,
121    Eq,
122    Ne,
123    Lt,
124    Gt,
125    Le,
126    Ge,
127    Shl,
128    Shr,
129    Add,
130    Sub,
131    Mul,
132    Div,
133    Rem,
134}
135
136impl Expr {
137    /// 문자열 식을 파싱한다. `H3|`/`C0|` operand 는 이 시점에 단위/명령으로 해석된다.
138    pub fn parse(src: &str) -> Result<Expr, ExprError> {
139        let tokens = lex(src)?;
140        let mut p = Parser { tokens, pos: 0 };
141        let e = p.parse_ternary()?;
142        if p.pos != p.tokens.len() {
143            return Err(ExprError::UnexpectedToken(format!("{:?}", p.tokens[p.pos])));
144        }
145        Ok(e)
146    }
147
148    /// 이 식(하위 포함)이 H3| 한글 낱자(Unit)를 만들 수 있는지. 한글 조합 항목과
149    /// 로마자/패스스루 항목을 구별하는 데 쓴다(한글 항목이면 KeyTable 에 H3| 가 있음).
150    pub fn contains_unit(&self) -> bool {
151        match self {
152            Expr::Unit(_) => true,
153            Expr::Int(_) | Expr::Var(_) | Expr::Command(_) => false,
154            Expr::Unary(_, x) => x.contains_unit(),
155            Expr::Binary(_, a, b) => a.contains_unit() || b.contains_unit(),
156            Expr::Ternary(c, t, f) => c.contains_unit() || t.contains_unit() || f.contains_unit(),
157        }
158    }
159
160    /// 문맥으로 식을 평가한다.
161    pub fn eval(&self, ctx: &Ctx) -> Result<Value, ExprError> {
162        Ok(match self {
163            Expr::Int(n) => Value::Int(*n),
164            Expr::Unit(u) => Value::Unit(*u),
165            Expr::Command(c) => Value::Command(*c),
166            Expr::Var(name) => Value::Int(
167                ctx.var(name)
168                    .ok_or_else(|| ExprError::UnknownVar(name.clone()))?,
169            ),
170            Expr::Unary(op, x) => {
171                let v = x.eval(ctx)?.as_int()?;
172                Value::Int(match op {
173                    UnOp::Not => (v == 0) as i64,
174                    UnOp::Neg => -v,
175                    UnOp::BitNot => !v,
176                })
177            }
178            Expr::Ternary(cond, t, f) => {
179                if cond.eval(ctx)?.truthy() {
180                    t.eval(ctx)?
181                } else {
182                    f.eval(ctx)?
183                }
184            }
185            Expr::Binary(op, l, r) => {
186                // 단축 평가 논리 연산
187                match op {
188                    BinOp::And => {
189                        return Ok(Value::Int(
190                            (l.eval(ctx)?.truthy() && r.eval(ctx)?.truthy()) as i64,
191                        ))
192                    }
193                    BinOp::Or => {
194                        return Ok(Value::Int(
195                            (l.eval(ctx)?.truthy() || r.eval(ctx)?.truthy()) as i64,
196                        ))
197                    }
198                    _ => {}
199                }
200                let a = l.eval(ctx)?.as_int()?;
201                let b = r.eval(ctx)?.as_int()?;
202                Value::Int(match op {
203                    BinOp::BitOr => a | b,
204                    BinOp::BitXor => a ^ b,
205                    BinOp::BitAnd => a & b,
206                    BinOp::Eq => (a == b) as i64,
207                    BinOp::Ne => (a != b) as i64,
208                    BinOp::Lt => (a < b) as i64,
209                    BinOp::Gt => (a > b) as i64,
210                    BinOp::Le => (a <= b) as i64,
211                    BinOp::Ge => (a >= b) as i64,
212                    BinOp::Shl => a << b,
213                    BinOp::Shr => a >> b,
214                    BinOp::Add => a + b,
215                    BinOp::Sub => a - b,
216                    BinOp::Mul => a * b,
217                    BinOp::Div => a / b,
218                    BinOp::Rem => a % b,
219                    BinOp::And | BinOp::Or => unreachable!(),
220                })
221            }
222        })
223    }
224}
225
226/// 한 번에 파싱하고 평가하는 편의 함수.
227pub fn eval_str(src: &str, ctx: &Ctx) -> Result<Value, ExprError> {
228    Expr::parse(src)?.eval(ctx)
229}
230
231// ── 렉서 ────────────────────────────────────────────────────────────────────
232
233#[derive(Clone, Debug, PartialEq, Eq)]
234enum Tok {
235    Num(i64),
236    Ident(String),
237    Pipe,
238    Question,
239    Colon,
240    OrOr,
241    AndAnd,
242    Caret,
243    Amp,
244    EqEq,
245    Ne,
246    Le,
247    Ge,
248    Shl,
249    Shr,
250    Lt,
251    Gt,
252    Plus,
253    Minus,
254    Star,
255    Slash,
256    Percent,
257    Bang,
258    Tilde,
259    LParen,
260    RParen,
261}
262
263fn lex(src: &str) -> Result<Vec<Tok>, ExprError> {
264    let chars: Vec<char> = src.chars().collect();
265    let mut i = 0;
266    let mut out = Vec::new();
267    while i < chars.len() {
268        let c = chars[i];
269        if c.is_whitespace() {
270            i += 1;
271            continue;
272        }
273        // 숫자 (0x.. 또는 10진)
274        if c.is_ascii_digit() {
275            let start = i;
276            if c == '0' && i + 1 < chars.len() && (chars[i + 1] == 'x' || chars[i + 1] == 'X') {
277                i += 2;
278                while i < chars.len() && chars[i].is_ascii_hexdigit() {
279                    i += 1;
280                }
281                let s: String = chars[start + 2..i].iter().collect();
282                let n = i64::from_str_radix(&s, 16)
283                    .map_err(|_| ExprError::UnexpectedToken(s.clone()))?;
284                out.push(Tok::Num(n));
285            } else {
286                i += 1;
287                while i < chars.len() && chars[i].is_ascii_digit() {
288                    i += 1;
289                }
290                let s: String = chars[start..i].iter().collect();
291                let n: i64 = s
292                    .parse()
293                    .map_err(|_| ExprError::UnexpectedToken(s.clone()))?;
294                out.push(Tok::Num(n));
295            }
296            continue;
297        }
298        // 식별자 (변수/태그/니모닉): [A-Za-z_][A-Za-z0-9_]*
299        if c.is_ascii_alphabetic() || c == '_' {
300            let start = i;
301            i += 1;
302            while i < chars.len() && (chars[i].is_ascii_alphanumeric() || chars[i] == '_') {
303                i += 1;
304            }
305            out.push(Tok::Ident(chars[start..i].iter().collect()));
306            continue;
307        }
308        // 연산자 (다문자 우선)
309        let two = |a: char, b: char| i + 1 < chars.len() && chars[i] == a && chars[i + 1] == b;
310        let tok = if two('|', '|') {
311            i += 2;
312            Tok::OrOr
313        } else if two('&', '&') {
314            i += 2;
315            Tok::AndAnd
316        } else if two('=', '=') {
317            i += 2;
318            Tok::EqEq
319        } else if two('!', '=') {
320            i += 2;
321            Tok::Ne
322        } else if two('<', '=') {
323            i += 2;
324            Tok::Le
325        } else if two('>', '=') {
326            i += 2;
327            Tok::Ge
328        } else if two('<', '<') {
329            i += 2;
330            Tok::Shl
331        } else if two('>', '>') {
332            i += 2;
333            Tok::Shr
334        } else {
335            let t = match c {
336                '|' => Tok::Pipe,
337                '?' => Tok::Question,
338                ':' => Tok::Colon,
339                '^' => Tok::Caret,
340                '&' => Tok::Amp,
341                '<' => Tok::Lt,
342                '>' => Tok::Gt,
343                '+' => Tok::Plus,
344                '-' => Tok::Minus,
345                '*' => Tok::Star,
346                '/' => Tok::Slash,
347                '%' => Tok::Percent,
348                '!' => Tok::Bang,
349                '~' => Tok::Tilde,
350                '(' => Tok::LParen,
351                ')' => Tok::RParen,
352                other => return Err(ExprError::UnexpectedChar(other, i)),
353            };
354            i += 1;
355            t
356        };
357        out.push(tok);
358    }
359    Ok(out)
360}
361
362// ── 파서 (재귀 하강 + 이항 우선순위 등반) ────────────────────────────────────
363
364struct Parser {
365    tokens: Vec<Tok>,
366    pos: usize,
367}
368
369impl Parser {
370    fn peek(&self) -> Option<&Tok> {
371        self.tokens.get(self.pos)
372    }
373    fn bump(&mut self) -> Option<Tok> {
374        let t = self.tokens.get(self.pos).cloned();
375        if t.is_some() {
376            self.pos += 1;
377        }
378        t
379    }
380    fn expect(&mut self, t: &Tok) -> Result<(), ExprError> {
381        match self.bump() {
382            Some(ref got) if got == t => Ok(()),
383            Some(got) => Err(ExprError::UnexpectedToken(format!("{got:?}"))),
384            None => Err(ExprError::UnexpectedEnd),
385        }
386    }
387
388    /// ternary := binary ( '?' ternary ':' ternary )?
389    fn parse_ternary(&mut self) -> Result<Expr, ExprError> {
390        let cond = self.parse_binary(0)?;
391        if let Some(Tok::Question) = self.peek() {
392            self.bump();
393            let then = self.parse_ternary()?;
394            self.expect(&Tok::Colon)?;
395            let els = self.parse_ternary()?;
396            Ok(Expr::Ternary(Box::new(cond), Box::new(then), Box::new(els)))
397        } else {
398            Ok(cond)
399        }
400    }
401
402    /// 이항 연산: 최소 우선순위 `min_prec` 이상만 흡수.
403    fn parse_binary(&mut self, min_prec: u8) -> Result<Expr, ExprError> {
404        let mut left = self.parse_unary()?;
405        while let Some(tok) = self.peek() {
406            let (op, prec) = match binop_of(tok) {
407                Some(v) => v,
408                None => break,
409            };
410            if prec < min_prec {
411                break;
412            }
413            self.bump();
414            // 모든 이항 연산자는 좌결합 → 오른쪽은 prec+1
415            let right = self.parse_binary(prec + 1)?;
416            left = Expr::Binary(op, Box::new(left), Box::new(right));
417        }
418        Ok(left)
419    }
420
421    /// unary := ('!' | '-' | '~')? unary | primary
422    fn parse_unary(&mut self) -> Result<Expr, ExprError> {
423        match self.peek() {
424            Some(Tok::Bang) => {
425                self.bump();
426                Ok(Expr::Unary(UnOp::Not, Box::new(self.parse_unary()?)))
427            }
428            Some(Tok::Minus) => {
429                self.bump();
430                Ok(Expr::Unary(UnOp::Neg, Box::new(self.parse_unary()?)))
431            }
432            Some(Tok::Tilde) => {
433                self.bump();
434                Ok(Expr::Unary(UnOp::BitNot, Box::new(self.parse_unary()?)))
435            }
436            _ => self.parse_primary(),
437        }
438    }
439
440    fn parse_primary(&mut self) -> Result<Expr, ExprError> {
441        match self.bump() {
442            Some(Tok::Num(n)) => Ok(Expr::Int(n)),
443            Some(Tok::LParen) => {
444                let e = self.parse_ternary()?;
445                self.expect(&Tok::RParen)?;
446                Ok(e)
447            }
448            Some(Tok::Ident(name)) => {
449                // 태그 H3| / C0| 처리
450                if (name == "H3" || name == "C0") && matches!(self.peek(), Some(Tok::Pipe)) {
451                    self.bump(); // '|'
452                    self.parse_tagged(&name)
453                } else {
454                    Ok(Expr::Var(name))
455                }
456            }
457            Some(other) => Err(ExprError::UnexpectedToken(format!("{other:?}"))),
458            None => Err(ExprError::UnexpectedEnd),
459        }
460    }
461
462    /// 태그 operand 를 읽어 단위/명령으로 해석. operand 는 숫자 또는 식별자(니모닉).
463    fn parse_tagged(&mut self, tag: &str) -> Result<Expr, ExprError> {
464        match self.bump() {
465            Some(Tok::Num(n)) => {
466                let n = n as u32;
467                if tag == "C0" {
468                    Ok(Expr::Command(n))
469                } else {
470                    let u = unit::resolve_numeric(n)
471                        .ok_or_else(|| ExprError::BadUnit(format!("0x{n:X}")))?;
472                    Ok(Expr::Unit(u))
473                }
474            }
475            Some(Tok::Ident(s)) => {
476                if tag == "C0" {
477                    Err(ExprError::BadCommand(s))
478                } else {
479                    let u = unit::resolve_mnemonic(&s, None).ok_or(ExprError::BadUnit(s))?;
480                    Ok(Expr::Unit(u))
481                }
482            }
483            Some(other) => Err(ExprError::UnexpectedToken(format!("{other:?}"))),
484            None => Err(ExprError::UnexpectedEnd),
485        }
486    }
487}
488
489/// 토큰 → (이항 연산자, 우선순위). 큰 값이 더 강하게 결합.
490fn binop_of(t: &Tok) -> Option<(BinOp, u8)> {
491    Some(match t {
492        Tok::OrOr => (BinOp::Or, 1),
493        Tok::AndAnd => (BinOp::And, 2),
494        Tok::Pipe => (BinOp::BitOr, 3),
495        Tok::Caret => (BinOp::BitXor, 4),
496        Tok::Amp => (BinOp::BitAnd, 5),
497        Tok::EqEq => (BinOp::Eq, 6),
498        Tok::Ne => (BinOp::Ne, 6),
499        Tok::Lt => (BinOp::Lt, 7),
500        Tok::Gt => (BinOp::Gt, 7),
501        Tok::Le => (BinOp::Le, 7),
502        Tok::Ge => (BinOp::Ge, 7),
503        Tok::Shl => (BinOp::Shl, 8),
504        Tok::Shr => (BinOp::Shr, 8),
505        Tok::Plus => (BinOp::Add, 9),
506        Tok::Minus => (BinOp::Sub, 9),
507        Tok::Star => (BinOp::Mul, 10),
508        Tok::Slash => (BinOp::Div, 10),
509        Tok::Percent => (BinOp::Rem, 10),
510        _ => return None,
511    })
512}
513
514#[cfg(test)]
515mod tests {
516    use super::*;
517    use crate::unit::{Category, Jamo};
518
519    fn ev(src: &str, ctx: &Ctx) -> Value {
520        eval_str(src, ctx).unwrap_or_else(|e| panic!("eval {src:?}: {e}"))
521    }
522
523    #[test]
524    fn literals_and_arith() {
525        let ctx = Ctx::default();
526        assert_eq!(ev("0xB7", &ctx), Value::Int(0xB7));
527        assert_eq!(ev("500", &ctx), Value::Int(500));
528        assert_eq!(ev("-2", &ctx), Value::Int(-2));
529        // C 우선순위: << 가 ^ 보다 강함 → 119 ^ (1<<5) = 119 ^ 32 = 87 = 'W'
530        let shifted = Ctx {
531            p: 1,
532            ..Default::default()
533        };
534        assert_eq!(ev("119^(P&1)<<5", &shifted), Value::Int('W' as i64));
535        let unshifted = Ctx {
536            p: 0,
537            ..Default::default()
538        };
539        assert_eq!(ev("119^(P&1)<<5", &unshifted), Value::Int('w' as i64));
540    }
541
542    #[test]
543    fn tagged_units() {
544        let ctx = Ctx::default();
545        // H3|_GG → 종성 ㄲ
546        assert_eq!(
547            ev("H3|_GG", &ctx),
548            Value::Unit(Unit::Jamo(Jamo::new(Category::Jong, 0x11A9)))
549        );
550        // H3|O_ → 중성 ㅗ
551        assert_eq!(
552            ev("H3|O_", &ctx),
553            Value::Unit(Unit::Jamo(Jamo::new(Category::Jung, 0x1169)))
554        );
555        // H3|0x820000 → 가상 단위 130
556        assert_eq!(ev("H3|0x820000", &ctx), Value::Unit(Unit::Virtual(130)));
557        // C0|0x82 → 한자 명령
558        assert_eq!(ev("C0|0x82", &ctx), Value::Command(0x82));
559    }
560
561    #[test]
562    fn ternary_with_t() {
563        // T ? H3|_J : 0x23  (조합 중이면 종성 ㅈ, 아니면 '#')
564        let composing = Ctx {
565            t: 1,
566            ..Default::default()
567        };
568        assert_eq!(
569            ev("T ? H3|_J : 0x23", &composing),
570            Value::Unit(Unit::Jamo(Jamo::new(Category::Jong, 0x11BD)))
571        );
572        let idle = Ctx {
573            t: 0,
574            ..Default::default()
575        };
576        assert_eq!(ev("T ? H3|_J : 0x23", &idle), Value::Int(0x23));
577        // T ? H3|0x1F4 : 0x24  ($ 키: 조합 중이면 갈마들이 토글)
578        assert_eq!(
579            ev("T ? H3|0x1F4 : 0x24", &composing),
580            Value::Unit(Unit::Toggle)
581        );
582        assert_eq!(ev("T ? H3|0x1F4 : 0x24", &idle), Value::Int(0x24));
583    }
584
585    #[test]
586    fn automata_expr_parses_and_evaluates() {
587        // 상태 2 식: A&&A!=500 ? 0 : B||C||A==500 ? 2 : -2
588        let src = "A&&A!=500 ? 0 : B||C||A==500 ? 2 : -2";
589        // 들어온 게 초성(A!=0,!=500) → 0
590        assert_eq!(
591            ev(
592                src,
593                &Ctx {
594                    a: 0x1100,
595                    ..Default::default()
596                }
597            ),
598            Value::Int(0)
599        );
600        // 들어온 게 종성(C!=0) → 2
601        assert_eq!(
602            ev(
603                src,
604                &Ctx {
605                    c: 0x11A8,
606                    ..Default::default()
607                }
608            ),
609            Value::Int(2)
610        );
611        // 토글(A==500) → 2
612        assert_eq!(
613            ev(
614                src,
615                &Ctx {
616                    a: 500,
617                    ..Default::default()
618                }
619            ),
620            Value::Int(2)
621        );
622        // 아무 낱자도 아님 → -2
623        assert_eq!(ev(src, &Ctx::default()), Value::Int(-2));
624    }
625
626    #[test]
627    fn contains_unit_detects_hangul_keys() {
628        // 한글 키: H3| 포함 → true
629        assert!(Expr::parse("H3|_GG").unwrap().contains_unit());
630        assert!(Expr::parse("T ? H3|_J : 0x23").unwrap().contains_unit());
631        // 로마자/리터럴 키: Unit 없음 → false
632        assert!(!Expr::parse("119^(P&1)<<5").unwrap().contains_unit());
633        assert!(!Expr::parse("0x5B").unwrap().contains_unit());
634        assert!(!Expr::parse("C0|0x82").unwrap().contains_unit());
635    }
636
637    #[test]
638    fn precedence_sanity() {
639        let ctx = Ctx::default();
640        assert_eq!(ev("1+2*3", &ctx), Value::Int(7));
641        assert_eq!(ev("(1+2)*3", &ctx), Value::Int(9));
642        assert_eq!(ev("1<2 && 3>2", &ctx), Value::Int(1));
643        assert_eq!(ev("0 || 0 || 5", &ctx), Value::Int(1)); // 논리 결과는 0/1
644        assert_eq!(ev("7 & 3", &ctx), Value::Int(3));
645        assert_eq!(ev("1 << 4", &ctx), Value::Int(16));
646    }
647}