Skip to main content

Crate ferrelex

Crate ferrelex 

Source
Expand description

§Ferrelex

Ferrelex is a crate that enables you to write powerful, Unicode-aware lexers entirely in Rust.

Ferrelex is a lexer generator: it leverages procedural macros to let you describe token patterns as readable regex expressions and match arms, then compiles them into an efficient DFA at compile time — no runtime regex engine, no heap allocation per token. You get the expressiveness of a traditional lexer generator tool (think ocamllex or flex) with full access to Rust’s type system, pattern matching, and error handling.

§Example

use ferrelex::{lexer::lex, lexbuf::{utf8::LexBuf, refiller::Utf8Refiller}};

// You can use any type you'd like as the return type of your lexer.
#[derive(Debug, PartialEq, Eq)]
enum Token {
    Ascii(String),
    Lambda,
    Eof,
    Invalid
}

// The lex! macro is where your regex definitions and match arms live.
lex! {
    // Regexes are constants of type `Regex`.
    const ASCII_LETTER: Regex = ('a'..='z') | ('A'..='Z');
    // Some characters don't fit in a Rust char literal — use a string instead.
    const LAMBDA: Regex = "λ";

    // Your lexer lives inside an ordinary Rust function. It must accept a
    // `&mut LexBuf` argument; the return type and any extra arguments are
    // entirely up to you.
    pub fn my_lexer(lexbuf: &mut LexBuf) -> Token {
        // Place `#[lexer]` before your match expression to activate the DFA.
        #[lexer]
        match lexbuf {
            ASCII_LETTER => Token::Ascii(lexbuf.lexeme()),
            LAMBDA => Token::Lambda,
            eof => Token::Eof,
            // The last arm must be a wildcard to handle unmatched input.
            _ => Token::Invalid,
        }
    }
}

fn main() {
    // Create a LexBuf from any input source — here, an owned String.
    let mut lexbuf = LexBuf::new(Utf8Refiller::new(String::from("λhello")));

    // Call your lexer function once per token. The LexBuf advances automatically.
    assert_eq!(my_lexer(&mut lexbuf), Token::Lambda);
    assert_eq!(my_lexer(&mut lexbuf), Token::Ascii(String::from("h")));
    assert_eq!(my_lexer(&mut lexbuf), Token::Ascii(String::from("e")));
    assert_eq!(my_lexer(&mut lexbuf), Token::Ascii(String::from("l")));
    assert_eq!(my_lexer(&mut lexbuf), Token::Ascii(String::from("l")));
    assert_eq!(my_lexer(&mut lexbuf), Token::Ascii(String::from("o")));

    // Once you reach the end of input, every call returns Token::Eof.
    assert_eq!(my_lexer(&mut lexbuf), Token::Eof);
    assert_eq!(my_lexer(&mut lexbuf), Token::Eof);
}

§Regex syntax

Regex expressions inside lex! use Rust syntax and are evaluated at compile time:

SyntaxMeaning
'a'Single character
"hello"Literal string — sequence of its characters
0x41Unicode code point as an integer literal
'a'..='z'Inclusive character range
'a'..'z'Exclusive character range (a to y)
r1 | r2Alternation — matches either r1 or r2
(r1, r2)Sequence — r1 followed by r2
Plus(r)One or more (r+)
Star(r)Zero or more (r*)
Opt(r)Zero or one (r?)
Rep(r, n..=m)Between n and m repetitions (inclusive)
Rep(r, n)Exactly n repetitions
Compl(r)Complement — any character not in r ¹
Sub(r1, r2)Set difference — characters in r1 but not in r2 ¹
Intersect(r1, r2)Set intersection of two character classes ¹
AnyOf("abc")Any single character from the given string
NAMENamed regex defined with const NAME: Regex = …

¹ Compl, Sub, and Intersect require both operands to be single-character-class regexes. Prefer char literals (e.g. '"') over single-character string literals (e.g. "\"") as arguments — a string literal creates a sequence internally and will be rejected by these operators.

§Built-in regex constants

The following names are always in scope inside lex!, without any const definition:

NameMatches
anyAny single Unicode scalar value (does not match EOF)
eofEnd of input
digit_ascii09
upper_asciiAZ
lower_asciiaz
alpha_asciiAZ and az
alnum_asciiAZ, az, and 09
whitespace_asciiSpace, tab (\t), newline (\n), carriage return (\r)
word_asciialnum_ascii plus _

All built-in shorthands are ASCII-only — the _ascii suffix makes that explicit. For full Unicode coverage, use Unicode category and property names directly as identifiers — see the sections below.

§Unicode General Categories

The two-letter Unicode General Category codes are available as identifiers directly inside lex!. Single-letter super-categories union all categories sharing that letter prefix.

§Specific categories

IdentifierLong nameDescription
CcControlC0/C1 control codes
CfFormatInvisible formatting indicators
CnUnassignedCode points not yet assigned
CoPrivate_UsePrivate-use code points
CsSurrogateSurrogate code points (U+D800–U+DFFF)
LlLowercase_LetterLowercase letters (az, à, α, …)
LmModifier_LetterModifier letters
LoOther_LetterOther letters (ideographs, syllables, …)
LtTitlecase_LetterDigraphic titlecase letters (e.g. )
LuUppercase_LetterUppercase letters (AZ, À, Α, …)
McSpacing_MarkSpacing combining marks
MeEnclosing_MarkEnclosing combining marks
MnNonspacing_MarkNon-spacing combining marks
NdDecimal_NumberDecimal digits (09, ٠٩, …)
NlLetter_NumberLetter-like numerics (Roman numerals, …)
NoOther_NumberOther numerics (fractions, superscripts, …)
PcConnector_PunctuationConnector punctuation (e.g. _)
PdDash_PunctuationDashes and hyphens
PeClose_PunctuationClosing brackets (), ], }, …)
PfFinal_PunctuationFinal quotation marks
PiInitial_PunctuationInitial quotation marks
PoOther_PunctuationOther punctuation (!, ., ,, …)
PsOpen_PunctuationOpening brackets ((, [, {, …)
ScCurrency_SymbolCurrency symbols ($, , £, …)
SkModifier_SymbolNon-spacing modifier symbols
SmMath_SymbolMathematical symbols (+, <, =, …)
SoOther_SymbolOther symbols
ZlLine_SeparatorLine separator (U+2028)
ZpParagraph_SeparatorParagraph separator (U+2029)
ZsSpace_SeparatorSpace characters (U+0020, U+00A0, …)

§Super-category aggregates

IdentifierExpands to
CCcCfCnCoCs
LLlLmLoLtLu
LCLlLtLu (cased letters only)
MMcMeMn
NNdNlNo
PPcPdPePfPiPoPs
SScSkSmSo
ZZlZpZs

§Unicode Derived Properties

The following Unicode derived core property names are available as identifiers inside lex! (Unicode 17.0.0, from DerivedCoreProperties.txt):

IdentifierDescription
AlphabeticLetters and letter-like characters considered alphabetic
CasedCharacters with an uppercase, lowercase, or titlecase form
Case_IgnorableCharacters that do not affect casing of surrounding text
Changes_When_LowercasedCharacters whose lowercased form differs
Changes_When_UppercasedCharacters whose uppercased form differs
Changes_When_TitlecasedCharacters whose titlecased form differs
Changes_When_CasefoldedCharacters whose case-folded form differs
Changes_When_CasemappedUnion of the three Changes_When_*cased sets
Default_Ignorable_Code_PointCode points that should be ignored by default
Grapheme_BaseCharacters that can be the base of a grapheme cluster
Grapheme_ExtendCharacters that extend a grapheme cluster
Grapheme_LinkDeprecated virama-based grapheme linking characters
ID_StartCharacters allowed at the start of an identifier
ID_ContinueCharacters allowed inside an identifier (after ID_Start)
XID_StartStable version of ID_Start (closure under NFKC)
XID_ContinueStable version of ID_Continue (closure under NFKC)
LowercaseCharacters with the Lowercase property
UppercaseCharacters with the Uppercase property
MathCharacters used in mathematical notation

§Skipping tokens with #[skip]

Annotate a match arm with #[skip] to consume the matched input and restart the DFA immediately, without returning to the caller. This is the idiomatic way to discard whitespace or comments:

#[lexer]
match lexbuf {
    #[skip] whitespace_ascii       => {}
    #[skip] ("//", Star(any))      => {}  // line comment
    IDENT => Token::Ident(lexbuf.lexeme()),
    eof   => Token::Eof,
    _     => Token::Error,
}

§Specialized lexer functions

Some tokens (string literals, block comments, heredocs, …) require different lexing rules mid-stream. The natural approach is to call a specialized lexer function defined in the same lex! block. To avoid stack overflows on long inputs, use loop { #[lexer] match … } inside the inner function instead of recursion:

lex! {
    const NOT_DQUOTE: Regex = Sub(any, '"');

    pub fn token(lexbuf: &mut LexBuf) -> Token {
        #[lexer]
        match lexbuf {
            '"'   => lex_string(lexbuf),
            IDENT => Token::Ident(lexbuf.lexeme()),
            eof   => Token::Eof,
            _     => Token::Error,
        }
    }

    // Inner lexer: iterative, no stack growth.
    pub fn lex_string(lexbuf: &mut LexBuf) -> Token {
        let mut acc = String::new();
        loop {
            #[lexer]
            match lexbuf {
                '"'        => return Token::Str(acc),
                NOT_DQUOTE => acc += lexbuf.lexeme_str(),
                eof | _    => return Token::Error,
            }
        }
    }
}

Note: each #[lexer] match resets the token start position. If you need the position of the opening ", save it before calling the inner function: let start = lexbuf.start_pos();

§#[lexer] options

The #[lexer] attribute accepts optional settings:

OptionEffect
no_line_trackingDisable line/column tracking for this match. Speeds up the DFA slightly; start_pos().line will always be 0.
allow_recursionSuppress the compile error for a direct recursive call to the enclosing function inside a match arm.
case_insensitiveFold all character sets so patterns match regardless of case. "select" will match SELECT, Select, etc. Applies to every arm in the match. Note: iterates over every code point at compile time — fast for ASCII keyword sets, may slow compilation on large Unicode categories.
#[lexer(no_line_tracking)]
match lexbuf {
    _ => ()
}

§Extracting the matched text

Inside a match arm body, the lexbuf argument exposes the matched input:

MethodReturns
lexbuf.lexeme()Owned String of the matched text
lexbuf.lexeme_str()Borrowed &str (zero-copy)
lexbuf.lexeme_bytes()Raw &[u8] — safe even on invalid UTF-8
lexbuf.lexeme_len()Number of Unicode scalar values matched
lexbuf.lexeme_char(i)The i-th character (0-indexed), or None
lexbuf.lexeme_chars()Iterator over the matched characters

§Position tracking

Token positions are tracked automatically. These methods are accurate inside an arm body, after a successful match:

MethodReturns
lexbuf.start_pos()Position — first character of the current token
lexbuf.end_pos()Position — character just past the token
lexbuf.location()Location combining start and end
lexbuf.set_filename(path)Attach a filename included in all subsequent positions
lexbuf.set_line(n)Override the tracked line number (e.g. after a #line N directive)

Position contains line (1-indexed), col (0-indexed character offset from the start of the line), and filename.

§Invalid UTF-8

When the input contains an invalid UTF-8 byte, the DFA fires the wildcard (_) arm and sets lexbuf.invalid_byte to Some(byte). Check this field to distinguish invalid bytes from valid-but-unmatched characters:

_ => match lexbuf.invalid_byte {
    Some(b) => Token::InvalidByte(b),
    None    => Token::UnexpectedChar,
}

lexbuf.invalid_byte is cleared at the start of each new match. Calling lexbuf.lexeme() or lexbuf.lexeme_str() in the wildcard arm when an invalid byte triggered it produces a deprecation warning at compile time; use lexbuf.lexeme_bytes() instead.

§Input sources

Four ready-to-use type aliases live in lexbuf::utf8:

Type aliasInput strategyChar cacheUse when
utf8::LexBufBuffered (streaming)NoneDefault. Files, stdin, owned String.
utf8::SliceLexBufZero-copy sliceNoneEntire input is already in memory as a &str or &[u8].
utf8::CachingLexBufBuffered (streaming)Vec<char>Streaming input with many multi-byte Unicode characters that are frequently backtracked over.
utf8::CachingSliceLexBufZero-copy sliceVec<char>In-memory input with many multi-byte Unicode characters and frequent backtracking.

utf8::LexBuf and utf8::CachingLexBuf are backed by a Refiller that feeds chunks of bytes into an internal buffer. Three Refiller implementations are provided:

TypeUse when
Utf8RefillerYou own a String
StrRefillerYou have a &str (borrows the source)
ReadRefillerYou have any std::io::Read — files, stdin, sockets, …

Implement Refiller yourself to support any other streaming source. For complete control over the input and cache strategies, use the underlying LexBuf<I, C> directly with your own Input and CharCache implementations.

Modules§

lexbuf
The input buffer, input source, and cache strategy types.
lexer
The lex! macro — the entry point for defining lexers.