re2c 是一个词法分析工具。目前支持 C, Go, Rust 三种目标语言。词法分析工具通过分析文本序列产生 Token,之后可以直接对 Token 进行分析或者将其输入到语法分析工具中。

介绍

re2c 以注释的形式写在目标语言源码中,然后通过 re2c/re2go/re2rust 进行预处理,将注释翻译成代码,就形成了目标产物:

fn lex(s: &[u8]) -> bool {
    let mut cursor = 0;
    /*!re2c
        re2c:define:YYCTYPE = u8;
        re2c:define:YYPEEK = "*s.get_unchecked(cursor)";
        re2c:define:YYSKIP = "cursor += 1;";
        re2c:yyfill:enable = 0;

        number = [1-9][0-9]*;

        number { return true; }
        *      { return false; }
    */
}

fn main() {
    assert!(lex(b"1234\0"));
}

然后使用 re2rust 将注释转换成代码:

$ re2rust lexer.rs -o lexer.rs

生成后的代码如下:

/* Generated by re2c */
// re2rust $INPUT -o $OUTPUT

fn lex(s: &[u8]) -> bool {
    let mut cursor = 0;

{
	#[allow(unused_assignments)]
	let mut yych : u8 = 0;
	let mut yystate : usize = 0;
	'yyl: loop {
		match yystate {
			0 => {
				yych = unsafe {*s.get_unchecked(cursor)};
				cursor += 1;
				match yych {
					0x31 ..= 0x39 => {
						yystate = 2;
						continue 'yyl;
					}
					_ => {
						yystate = 1;
						continue 'yyl;
					}
				}
			}
			1 => { return false; }
			2 => {
				yych = unsafe {*s.get_unchecked(cursor)};
				match yych {
					0x30 ..= 0x39 => {
						cursor += 1;
						yystate = 2;
						continue 'yyl;
					}
					_ => {
						yystate = 3;
						continue 'yyl;
					}
				}
			}
			3 => { return true; }
			_ => {
				panic!("internal lexer error")
			}
		}
	}
}

}

fn main() {
    assert!(lex(b"1234\0"));
}

语法

re2c 的语句有三种:

<name> = <regular expression>;

将 <name> 绑定到 regex 上。name 由英文字母、数字和下划线构成。定义后 name 可以在其它 regex 和 rule 中使用。

<configuration> = <value>;

修改 re2c 的 配置

<regular expression> { <code> }

若 regex 匹配,则执行 code。如果多个 regex 都匹配,则选择最长的 token,若 token 一样长,则选择最早匹配上的。

!<directive>;

指令是特殊的、预定义的语句。

程序接口

要使 re2c 能够正常工作,需要对 re2c 进行配置。re2c 支持两种风格:

  • 指针 API:主要用于 C 语言这种有指针的语言中。使用更简单。

  • 通用 API:用于无指针的语言中。

指针 API 实际上相当于预配置的通用 API:

/*!re2c
  re2c:define:YYPEEK       = "*buffer.get_unchecked(cursor)";
  re2c:define:YYSKIP       = "cursor += 1;";
  re2c:define:YYBACKUP     = "marker = cursor;";
  re2c:define:YYRESTORE    = "cursor = marker;";
  re2c:define:YYBACKUPCTX  = "ctxmarker = cursor;";
  re2c:define:YYRESTORECTX = "cursor = ctxmarker;";
  re2c:define:YYRESTORETAG = "cursor = @@{tag};";
  re2c:define:YYLESSTHAN   = "limit - cursor < @@{len}";
  re2c:define:YYSTAGP      = "@@{tag} = cursor;";
  re2c:define:YYSTAGN      = "@@{tag} = NONE;";
  re2c:define:YYSHIFT      = "cursor = (cursor as isize + @@{shift}) as usize;";
  re2c:define:YYSHIFTSTAG  = "@@{tag} = (@@{tag} as isize + @@{shift}) as usize;";
*/
当缺少配置时 re2c 不会报错,但是翻译后的代码一般无法编译。

API 原语

原语作用

YYCTYPE

字符(code unit)的类型。ASCII, UTF-8 都是 u8 类型。

YYPEEK

返回当前位置字符的值。

YYSKIP

将当前指针进行自增。

处理流 EOF

解析过程中需要知道合适终止解析,一般有三种方法:

  • 使用哨兵字符。

  • 使用边界检查。

  • 同时使用两者。

子模式提取

re2c 支持两种子模式提取方式:

  • 使用 tag。

  • 使用 posix captures。

tag 分为两种: @tag 形式的被称为 s-tag, #tag 的形式被成为 m-tag。其中 s-tag 代表了 tag 的单个值,而 m-tag 代表了匹配的整个历史。

下面演示了使用 s-tag 提取表达式的方式:

// re2rust $INPUT -o $OUTPUT

#[derive(Debug, PartialEq)]
struct SemVer(u32, u32, u32); // version: (major, minor, patch)

fn s2n(str: &[u8]) -> u32 { // convert a pre-parsed string to a number
    let mut n = 0;
    for i in str { n = n * 10 + *i as u32 - 48; }
    return n;
}

fn parse(str: &[u8]) -> Option<SemVer> {
    let (mut cur, mut mar) = (0, 0);

    // User-defined tag variables that are available in semantic action.
    let (t1, mut t2, t3, t4, t5);

    // Autogenerated tag variables used by the lexer to track tag values.
    const NONE: usize = std::usize::MAX;
    /*!stags:re2c format = 'let mut @@{tag} = NONE;'; */

    /*!re2c
        re2c:define:YYCTYPE     = u8;
        re2c:define:YYPEEK      = "*str.get_unchecked(cur)";
        re2c:define:YYSKIP      = "cur += 1;";
        re2c:define:YYBACKUP    = "mar = cur;";
        re2c:define:YYRESTORE   = "cur = mar;";
        re2c:define:YYSTAGP     = "@@{tag} = cur;";
        re2c:define:YYSTAGN     = "@@{tag} = NONE;";
        re2c:define:YYSHIFTSTAG = "@@{tag} -= -@@{shift}isize as usize;";
        re2c:yyfill:enable = 0;
        re2c:tags = 1;

        num = [0-9]+;

        @t1 num @t2 "." @t3 num @t4 ("." @t5 num)? [\x00] {
            let major = s2n(&str[t1..t2]);
            let minor = s2n(&str[t3..t4]);
            let patch = if t5 != NONE {s2n(&str[t5..cur - 1])} else {0};
            return Some(SemVer(major, minor, patch));
        }
        * { return None; }
    */
}

fn main() {
    assert_eq!(parse(b"23.34\0"), Some(SemVer(23, 34, 0)));
    assert_eq!(parse(b"1.2.99999\0"), Some(SemVer(1, 2, 99999)));
    assert_eq!(parse(b"1.a\0"), None);
}

故障处理

在解析过程中,YYPEEK 指向的实际上是需要解析的下一个字符,如果需要获取当前字符,需要对 cursor 进行偏移。此外,还需要谨慎处理终止条件:

Details
#[derive(Debug, Clone)]
enum WildChar {
    RepeatAny,
    Any,
    Choose(bool, usize, usize),
    Range(u8, u8),
    Char(u8),
    Eof,
}

/// # Errors
/// return error if pat is invalid.
pub fn sqlmatch(pat: &[u8], text: &[u8]) -> Result<bool, ()> {
    let mut cursor = 0usize;
    let mut text_cursor = 0usize;

    loop {
        let mut state = WildChar::Eof;
        let mut marker = cursor;
        let mut t1 = 0usize;
        let mut t2 = 0usize;
        /*!stags:re2c format = 'let mut @@{tag} = 0;'; */
        /*!re2c
            re2c:define:YYCTYPE     = u8;
            re2c:define:YYPEEK      = "match cursor < pat.len() { true => pat[cursor], false => 0}";
            re2c:define:YYSKIP      = "cursor += 1;";
            re2c:define:YYBACKUP    = "marker = cursor;";
            re2c:define:YYRESTORE   = "cursor = marker;";
            re2c:define:YYSTAGP     = "@@{tag} = cursor;";
            re2c:define:YYSHIFTSTAG = "@@{tag} -= -@@{shift}isize as usize;";
            re2c:tags               = 1;
            re2c:yyfill:enable      = 0;
            re2c:define:YYLESSTHAN  = "cursor >= pat.len()";
            re2c:eof = 0;

            neg_choose = "["[\\^!][^\]]*"]";
            choose = "["[^\]]*"]";
            alpha = [a-zA-Z];
            escaped_ch = "\\" .;

            $ {
                state = WildChar::Eof;
                break;
            }
            escaped_ch  {
                state = WildChar::Char(pat[cursor - 1]);
                break;
            }
            "%" {
                state = WildChar::RepeatAny;
                break;
            }
            "_" {
                state = WildChar::Any;
                break;
            }
            @t1 neg_choose @t2 {
                state = WildChar::Choose(true, t1 + 2, t2 - 1);
                break;
            }
            @t1 choose @t2 {
                state = WildChar::Choose(false, t1 + 1, t2 - 1);
                break;
            }
            @t1 alpha "-" alpha @t2 {
                state = WildChar::Range(pat[t1], pat[t2 - 1]);
                break;
            }
            alpha {
                state = WildChar::Char(pat[cursor - 1]);
                break;
            }
            * {
                unreachable!();
            }
        */

        macro_rules! YYPEEK {
            () => {
                if text_cursor == text.len() {
                    return Ok(false);
                } else {
                    text[text_cursor]
                }
            };
        }
        match state {
            WildChar::Choose(neg, s, e) => {
            }
            WildChar::Range(s, e) => {
            }
            WildChar::Char(ch) => {
            }
            WildChar::Eof => {
            }
            WildChar::Any => {
            }
            WildChar::RepeatAny => {
            }
        }
    }

    if text_cursor == text.len() {
        return Ok(true);
    }

    Ok(false)
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_sqlmatch(){
        assert!(sqlmatch(b"abcdefg", b"abcdefg").unwrap());
        assert!(sqlmatch(b"ab_", b"abc").unwrap());
        assert!(sqlmatch(b"ab%", b"abcdefg").unwrap());
        assert!(sqlmatch(b"%fg", b"%fg").unwrap());
        assert!(sqlmatch(b"a-z", b"a").unwrap());
        assert!(sqlmatch(b"[abc]", b"a").unwrap());
        assert!(sqlmatch(b"[^abc]", b"d").unwrap());
        assert!(sqlmatch(b"\\[[^abc]", b"[d").unwrap());
        assert!(sqlmatch(b"\\[abc", b"[abc").unwrap());
        assert!(sqlmatch(b"\\\\abc", b"\\abc").unwrap());
    }
}

UTF8 支持

re2c 对字符集提供了天然的支持。因此可以配合 Rust 提供 UTF8 支持。Rust 主要用到的 API 有:

功能方法

获取字符的个数

let text_len = text.chars().count();

获取第 n 个字符

#[inline(always)]
fn char_of(text: &str, pos: usize) -> u32 {
    text.chars().nth(pos).unwrap() as u32
}

re2c 中的 API 需要更改为:

/*!re2c
    re2c:define:YYCTYPE = u32;
*/

最终效果如下:

Details
#[derive(Debug, Clone)]
enum WildCard {
    RepeatAny,
    Any,
    Choose(bool, usize, usize),
    Range(u32, u32),
    Char(u32),
    Eof,
}

#[inline(always)]
fn char_of(text: &str, pos: usize) -> u32 {
    text.chars().nth(pos).unwrap() as u32
}

/// # Errors
/// return error if pat is invalid.
pub fn sqlmatch(pat: &str, text: &str) -> Result<bool, ()> {
    let pat_len = pat.chars().count();
    let text_len = text.chars().count();
    let mut cursor = 0usize;
    let mut text_cursor = 0usize;

    loop {
        let mut state = WildCard::Eof;
        let mut marker = cursor;
        let mut t1 = 0usize;
        let mut t2 = 0usize;
        /*!stags:re2c format = 'let mut @@{tag} = 0;'; */
        /*!re2c
            re2c:define:YYCTYPE     = u32;
            re2c:define:YYPEEK      = "match cursor < pat_len { true => char_of(pat, cursor), false => 0}";
            re2c:define:YYSKIP      = "cursor += 1;";
            re2c:define:YYBACKUP    = "marker = cursor;";
            re2c:define:YYRESTORE   = "cursor = marker;";
            re2c:define:YYSTAGP     = "@@{tag} = cursor;";
            re2c:define:YYSHIFTSTAG = "@@{tag} -= -@@{shift}isize as usize;";
            re2c:tags               = 1;
            re2c:yyfill:enable      = 0;
            re2c:define:YYLESSTHAN  = "cursor >= pat_len";
            re2c:eof = 0;

            neg_choose = "["[\\^!][^\]]*"]";
            choose = "["[^\]]*"]";
            word = [^\-];
            escaped_ch = "\\" .;

            $ {
                state = WildCard::Eof;
                break;
            }
            escaped_ch  {
                state = WildCard::Char(char_of(pat, cursor - 1));
                break;
            }
            "%" {
                state = WildCard::RepeatAny;
                break;
            }
            "_" {
                state = WildCard::Any;
                break;
            }
            @t1 "[" word "-" word "]" @t2 {
                state = WildCard::Range(char_of(pat, t1 + 1), char_of(pat, t2 - 2));
                break;
            }
            @t1 neg_choose @t2 {
                state = WildCard::Choose(true, t1 + 2, t2 - 1);
                break;
            }
            @t1 choose @t2 {
                state = WildCard::Choose(false, t1 + 1, t2 - 1);
                break;
            }
            * {
                state = WildCard::Char(char_of(pat, cursor - 1));
                break;
            }
        */

        macro_rules! YYPEEK {
            () => {
                if text_cursor == text_len {
                    return Ok(false);
                } else {
                    char_of(text, text_cursor)
                }
            };
        }
        match state {
            WildCard::Choose(neg, s, e) => {
            }
            WildCard::Range(s, e) => {
            }
            WildCard::Char(ch) => {
            }
            WildCard::Eof => {
            }
            WildCard::Any => {
            }
            WildCard::RepeatAny => {
            }
        }
    }

    if text_cursor == text_len {
        return Ok(true);
    }

    Ok(false)
}
Last moify: 2024-08-01 10:06:36
Build time:2025-07-18 09:41:42
Powered By asphinx