neural/inf/rs/lex/src/lib.rs

//! Tokenizer for the inf surface (specs/grammar.md).
//!
//! `:=` is a single token; a bare `:` is the bind separator and also the lead of
//! a program option (`:limit`, `:link`, โ€ฆ) โ€” the parser disambiguates by
//! position, so the lexer keeps `:` simple. `//` runs to end of line.

#[derive(Clone, Debug, PartialEq)]
pub enum Tok {
    Ident(String),
    Int(i64),
    Str(String),
    Assign,     // :=
    FixedArrow, // <~
    Question,   // ?
    LBracket,
    RBracket,
    LBrace,
    RBrace,
    LParen,
    RParen,
    Comma,
    Colon,
    Semi,
    Dot,
    Eq,
    Plus,
    Minus,
    Star,
    Hash,  // #
    At,    // @
    Tilde, // ~
    Slash, // /
}

#[derive(Clone, Debug, PartialEq)]
pub struct Spanned {
    pub tok: Tok,
    pub line: usize,
    pub col: usize,
}

#[derive(Clone, Debug, PartialEq)]
pub struct LexError {
    pub msg: String,
    pub line: usize,
    pub col: usize,
}

pub fn lex(src: &str) -> Result<Vec<Spanned>, LexError> {
    let b: Vec<char> = src.chars().collect();
    let mut i = 0usize;
    let mut line = 1usize;
    let mut col = 1usize;
    let mut out = Vec::new();

    let advance = |i: &mut usize, _line: &mut usize, col: &mut usize, n: usize| {
        for _ in 0..n {
            *i += 1;
            *col += 1;
        }
    };

    while i < b.len() {
        let c = b[i];
        let (sl, sc) = (line, col);
        match c {
            ' ' | '\t' | '\r' => advance(&mut i, &mut line, &mut col, 1),
            '\n' => {
                i += 1;
                line += 1;
                col = 1;
            }
            '/' if i + 1 < b.len() && b[i + 1] == '/' => {
                while i < b.len() && b[i] != '\n' {
                    i += 1;
                }
            }
            '/' => {
                out.push(Spanned { tok: Tok::Slash, line: sl, col: sc });
                advance(&mut i, &mut line, &mut col, 1);
            }
            ':' if i + 1 < b.len() && b[i + 1] == '=' => {
                out.push(Spanned { tok: Tok::Assign, line: sl, col: sc });
                advance(&mut i, &mut line, &mut col, 2);
            }
            '<' if i + 1 < b.len() && b[i + 1] == '~' => {
                out.push(Spanned { tok: Tok::FixedArrow, line: sl, col: sc });
                advance(&mut i, &mut line, &mut col, 2);
            }
            '[' | ']' | '{' | '}' | '(' | ')' | ',' | ':' | ';' | '.' | '=' | '+' | '-' | '*'
            | '#' | '@' | '~' | '?' => {
                let t = match c {
                    '[' => Tok::LBracket,
                    ']' => Tok::RBracket,
                    '{' => Tok::LBrace,
                    '}' => Tok::RBrace,
                    '(' => Tok::LParen,
                    ')' => Tok::RParen,
                    ',' => Tok::Comma,
                    ':' => Tok::Colon,
                    ';' => Tok::Semi,
                    '.' => Tok::Dot,
                    '=' => Tok::Eq,
                    '+' => Tok::Plus,
                    '-' => Tok::Minus,
                    '*' => Tok::Star,
                    '#' => Tok::Hash,
                    '@' => Tok::At,
                    '~' => Tok::Tilde,
                    '?' => Tok::Question,
                    _ => unreachable!(),
                };
                out.push(Spanned { tok: t, line: sl, col: sc });
                advance(&mut i, &mut line, &mut col, 1);
            }
            '"' => {
                advance(&mut i, &mut line, &mut col, 1);
                let mut s = String::new();
                while i < b.len() && b[i] != '"' {
                    if b[i] == '\\' && i + 1 < b.len() {
                        advance(&mut i, &mut line, &mut col, 1);
                        let e = b[i];
                        s.push(match e {
                            'n' => '\n',
                            't' => '\t',
                            other => other,
                        });
                    } else if b[i] == '\n' {
                        return Err(LexError { msg: "newline in string".into(), line, col });
                    } else {
                        s.push(b[i]);
                    }
                    advance(&mut i, &mut line, &mut col, 1);
                }
                if i >= b.len() {
                    return Err(LexError { msg: "unterminated string".into(), line: sl, col: sc });
                }
                advance(&mut i, &mut line, &mut col, 1); // closing quote
                out.push(Spanned { tok: Tok::Str(s), line: sl, col: sc });
            }
            d if d.is_ascii_digit() => {
                let mut n = String::new();
                while i < b.len() && b[i].is_ascii_digit() {
                    n.push(b[i]);
                    advance(&mut i, &mut line, &mut col, 1);
                }
                let v: i64 = n.parse().map_err(|_| LexError {
                    msg: format!("integer out of range: {n}"),
                    line: sl,
                    col: sc,
                })?;
                out.push(Spanned { tok: Tok::Int(v), line: sl, col: sc });
            }
            a if a.is_ascii_alphabetic() || a == '_' => {
                let mut id = String::new();
                while i < b.len() && (b[i].is_ascii_alphanumeric() || b[i] == '_') {
                    id.push(b[i]);
                    advance(&mut i, &mut line, &mut col, 1);
                }
                out.push(Spanned { tok: Tok::Ident(id), line: sl, col: sc });
            }
            other => {
                return Err(LexError {
                    msg: format!("unexpected character: {other:?}"),
                    line: sl,
                    col: sc,
                });
            }
        }
    }
    Ok(out)
}

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

    fn toks(s: &str) -> Vec<Tok> {
        lex(s).unwrap().into_iter().map(|t| t.tok).collect()
    }

    #[test]
    fn assign_vs_colon() {
        assert_eq!(
            toks("?[x] := a"),
            vec![
                Tok::Question,
                Tok::LBracket,
                Tok::Ident("x".into()),
                Tok::RBracket,
                Tok::Assign,
                Tok::Ident("a".into()),
            ]
        );
        // bind colon and option colon are the same token; the parser splits them
        assert_eq!(toks("to: p"), vec![Tok::Ident("to".into()), Tok::Colon, Tok::Ident("p".into())]);
        assert_eq!(toks(":limit"), vec![Tok::Colon, Tok::Ident("limit".into())]);
    }

    #[test]
    fn sigils_and_paths() {
        assert_eq!(
            toks("#cyber/truth @me ~name"),
            vec![
                Tok::Hash,
                Tok::Ident("cyber".into()),
                Tok::Slash,
                Tok::Ident("truth".into()),
                Tok::At,
                Tok::Ident("me".into()),
                Tok::Tilde,
                Tok::Ident("name".into()),
            ]
        );
    }

    #[test]
    fn comments_and_strings() {
        assert_eq!(toks("a // comment\n b"), vec![Tok::Ident("a".into()), Tok::Ident("b".into())]);
        assert_eq!(toks(r#""hello""#), vec![Tok::Str("hello".into())]);
    }

    #[test]
    fn ints_and_minus() {
        assert_eq!(toks("-3 42"), vec![Tok::Minus, Tok::Int(3), Tok::Int(42)]);
    }
}

Homonyms

cyb/optica/src/lib.rs
soft3/strata/src/lib.rs
cyb/honeycrisp/src/lib.rs
warriors/trisha/honeycrisp/lib.rs
warriors/trisha/wgpu/lib.rs
soft3/glia/import/lib.rs
soft3/foculus/src/lib.rs
soft3/nox/rs/lib.rs
soft3/cybergraph/src/lib.rs
soft3/tru/rs/lib.rs
soft3/mudra/src/lib.rs
soft3/glia/run/lib.rs
cyb/prysm/rs/lib.rs
warriors/trisha/rs/lib.rs
cyb/src-tauri/src/lib.rs
soft3/mir/src/lib.rs
soft3/lens/src/lib.rs
neural/trident/src/lib.rs
neural/rune/rs/subject/lib.rs
cyb/cyb/cyb-services/src/lib.rs
soft3/strata/nebu/rs/lib.rs
soft3/lens/core/src/lib.rs
neural/rs/mir-format/src/lib.rs
soft3/zheng/rs/src/lib.rs
neural/rune/rs/interp/lib.rs
soft3/radio/iroh-willow/src/lib.rs
neural/rune/rs/parse/lib.rs
neural/eidos/rs/src/lib.rs
neural/rs/darwin-sys/src/lib.rs
soft3/radio/iroh-gossip/src/lib.rs
soft3/radio/iroh-ffi/src/lib.rs
soft3/radio/iroh-car/src/lib.rs
soft3/radio/iroh-relay/src/lib.rs
soft3/bbg/rs/src/lib.rs
soft3/radio/iroh-docs/src/lib.rs
soft3/lens/ikat/src/lib.rs
neural/rune/rs/lex/lib.rs
cyb/honeycrisp/aruminium/src/lib.rs
soft3/hemera/rs/src/lib.rs
neural/rune/rs/ast/lib.rs
soft3/radio/iroh-blobs/src/lib.rs
cyb/honeycrisp/acpu/src/lib.rs
soft3/lens/porphyry/src/lib.rs
cyb/honeycrisp/rane/src/lib.rs
neural/rune/rs/compile/lib.rs
neural/rune/rs/parse-pure/lib.rs
neural/rs/codegen/src/lib.rs
soft3/lens/binius/src/lib.rs
neural/rune/rs/prysm/lib.rs
neural/rs/link/src/lib.rs
neural/rune/rs/mold/lib.rs
soft3/strata/proof/src/lib.rs
soft3/lens/brakedown/src/lib.rs
soft3/strata/kuro/rs/lib.rs
soft3/lens/assayer/src/lib.rs
neural/rs/core/src/lib.rs
neural/rs/macros/src/lib.rs
soft3/radio/cyber-bao/src/lib.rs
soft3/strata/compute/src/lib.rs
soft3/radio/iroh-base/src/lib.rs
soft3/radio/iroh-dns-server/src/lib.rs
neural/rune/rs/lower/lib.rs
soft3/strata/ext/src/lib.rs
soft3/strata/core/src/lib.rs
soft3/hemera/wgsl/src/lib.rs
soft3/radio/iroh/src/lib.rs
cyb/honeycrisp/unimem/src/lib.rs
cyb/evy/crates/evy_engine_tasks/src/lib.rs
cyb/evy/crates/evy_dialect/src/lib.rs
cyb/wysm/crates/wasi/src/lib.rs
cyb/wysm/crates/fuzz/src/lib.rs
soft3/strata/genies/rs/src/lib.rs
cyb/evy/crates/evy_platform_caps/src/lib.rs
neural/inf/rs/oracle/src/lib.rs
soft3/strata/jali/wgsl/src/lib.rs
cyb/evy/forks/bevy_transform/src/lib.rs
soft3/tape/impl/rust/src/lib.rs
cyb/wysm/crates/wasmi/src/lib.rs
cyb/evy/forks/bevy_render/src/lib.rs
cyb/evy/crates/evy_ecs_storage/src/lib.rs
cyb/evy/forks/naga/src/lib.rs
soft3/strata/trop/wgsl/src/lib.rs
cyb/wysm/crates/c_api/artifact/lib.rs
cyb/evy/forks/bevy_ecs/src/lib.rs
cyb/wysm/crates/ir/src/lib.rs
cyb/evy/forks/bevy_animation/src/lib.rs
cyb/evy/forks/bevy_sprite_render/src/lib.rs
cyb/wysm/crates/c_api/src/lib.rs
neural/inf/rs/parse/src/lib.rs
soft3/strata/trop/rs/src/lib.rs
soft3/strata/kuro/wgsl/src/lib.rs
neural/trident/editor/zed/src/lib.rs
cyb/evy/forks/bevy_mesh/src/lib.rs
cyb/evy/crates/evy_radio/src/lib.rs
cyb/evy/forks/bevy_anti_alias/src/lib.rs
soft3/strata/jali/rs/src/lib.rs
cyb/wysm/crates/wast/src/lib.rs
neural/inf/rs/plan/src/lib.rs
neural/rs/tests/macro-integration/src/lib.rs
soft3/radio/iroh-ffi/iroh-js/src/lib.rs
cyb/evy/forks/bevy_image/src/lib.rs
cyb/evy/forks/bevy_post_process/src/lib.rs
neural/inf/rs/source/src/lib.rs
cyb/wysm/crates/core/src/lib.rs
cyb/evy/crates/evy_diagnostic/src/lib.rs
cyb/evy/crates/evy_engine_dispatch/src/lib.rs
cyb/evy/forks/bevy_pbr/src/lib.rs
cyb/evy/forks/bevy_gizmos/src/lib.rs
cyb/evy/forks/bevy_gizmos_render/src/lib.rs
soft3/radio/iroh/bench/src/lib.rs
neural/inf/rs/ast/src/lib.rs
soft3/strata/genies/wgsl/src/lib.rs
soft3/strata/nebu/wgsl/src/lib.rs
cyb/wysm/crates/collections/src/lib.rs
neural/inf/rs/lower/src/lib.rs
cyb/evy/forks/bevy_sprite/src/lib.rs
cyb/evy/forks/bevy_diagnostic/src/lib.rs
neural/inf/rs/eval/src/lib.rs
cyb/wysm/crates/c_api/macro/lib.rs
cyb/evy/forks/bevy_tasks/src/lib.rs
cyb/evy/forks/bevy_core_pipeline/src/lib.rs
cyb/evy/crates/evy_prysm_core/src/lib.rs
neural/inf/rs/value/src/lib.rs
cyb/evy/crates/evy_engine_core/src/lib.rs
soft3/radio/tests/integration/src/lib.rs
bootloader/go-cyber/cw/packages/cyber-std-test/src/lib.rs
bootloader/go-cyber/cw/contracts/std-test/src/lib.rs
bootloader/go-cyber/cw/contracts/graph-filter/src/lib.rs
bootloader/go-cyber/cw/packages/cyber-std/src/lib.rs

Graph