mod keyword;
pub mod token;
pub use keyword::{is_forbidden_keyword, lookup_keyword};
pub use token::{Token, TokenKind};
use crate::types::{DiagCode, DiagnosticConfig, Span, SpanDiagnostic};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LexerState {
Normal,
InMacro,
InExports,
InComment,
}
pub struct Lexer<'src, 'cfg> {
source: &'src [u8],
pos: usize,
state: LexerState,
comment_start: usize,
diagnostics: Vec<SpanDiagnostic>,
diag_config: &'cfg DiagnosticConfig,
}
impl<'src, 'cfg> Lexer<'src, 'cfg> {
pub fn new(source: &'src [u8], diag_config: &'cfg DiagnosticConfig) -> Self {
Lexer {
source,
pos: 0,
state: LexerState::Normal,
comment_start: 0,
diagnostics: Vec::new(),
diag_config,
}
}
pub fn tokenize(mut self) -> (Vec<Token>, Vec<SpanDiagnostic>) {
let estimated = (self.source.len() / 6).max(64);
let mut tokens: Vec<Token> = Vec::with_capacity(estimated);
tokens.extend(&mut self);
tokens.push(Token {
kind: TokenKind::Eof,
span: self.span_from(self.pos),
});
(tokens, self.diagnostics)
}
pub fn next_token(&mut self) -> Token {
loop {
match self.state {
LexerState::InComment => return self.emit_comment(),
LexerState::InMacro => return self.skip_macro_body(),
LexerState::InExports => return self.skip_exports_body(),
LexerState::Normal => {
if let Some(tok) = self.next_normal_token() {
return tok;
}
}
}
}
}
fn is_eof(&self) -> bool {
self.pos >= self.source.len()
}
fn peek(&self) -> Option<u8> {
self.source.get(self.pos).copied()
}
fn peek_at(&self, offset: usize) -> Option<u8> {
self.source.get(self.pos + offset).copied()
}
fn advance(&mut self) -> Option<u8> {
let b = self.source.get(self.pos).copied()?;
self.pos += 1;
Some(b)
}
fn skip_whitespace(&mut self) {
while self
.peek()
.is_some_and(|b| matches!(b, b' ' | b'\t' | b'\r' | b'\n'))
{
self.advance();
}
}
fn skip_line_ending(&mut self) {
if let Some(b) = self.advance()
&& b == b'\r'
&& self.peek() == Some(b'\n')
{
self.advance();
}
}
fn skip_to_eol(&mut self) {
while let Some(b) = self.peek() {
if b == b'\n' || b == b'\r' {
self.skip_line_ending();
return;
}
self.advance();
}
}
fn span_from(&self, start: usize) -> Span {
Span::from_usize_offsets(start, self.pos)
}
fn token(&self, kind: TokenKind, start: usize) -> Token {
Token {
kind,
span: self.span_from(start),
}
}
fn remaining(&self) -> &[u8] {
&self.source[self.pos..]
}
fn is_comment_start(&self) -> bool {
self.remaining().starts_with(b"--")
}
fn emit_diagnostic(&mut self, code: DiagCode, span: Span, message: impl Into<String>) {
let severity = code.severity();
if !self.diag_config.should_report(code) {
return;
}
self.diagnostics.push(SpanDiagnostic {
code,
severity,
span,
message: message.into(),
});
}
fn next_normal_token(&mut self) -> Option<Token> {
self.skip_whitespace();
let start = self.pos;
let b = match self.peek() {
None => return Some(self.token(TokenKind::Eof, start)),
Some(b) => b,
};
if self.is_comment_start() {
self.comment_start = start;
self.pos += 2;
self.state = LexerState::InComment;
return None;
}
if let Some(kind) = punctuation_kind(b) {
self.advance();
return Some(self.token(kind, start));
}
if b == b'.' {
self.advance();
if self.peek() == Some(b'.') {
self.advance();
return Some(self.token(TokenKind::DotDot, start));
}
return Some(self.token(TokenKind::Dot, start));
}
if b == b':' {
self.pos += 1;
if self.remaining().starts_with(b":=") {
self.pos += 2;
return Some(self.token(TokenKind::ColonColonEqual, start));
}
return Some(self.token(TokenKind::Colon, start));
}
if b == b'-' {
if let Some(next) = self.peek_at(1)
&& next.is_ascii_digit()
{
return Some(self.scan_negative_number());
}
self.advance();
return Some(self.token(TokenKind::Minus, start));
}
if b.is_ascii_digit() {
return Some(self.scan_number());
}
if b == b'"' {
return Some(self.scan_quoted_string());
}
if b == b'\'' {
return Some(self.scan_hex_or_bin_string());
}
if b.is_ascii_alphabetic() {
return Some(self.scan_identifier_or_keyword());
}
self.advance();
let span = self.span_from(start);
self.emit_diagnostic(
DiagCode::UnexpectedCharacter,
span,
format!("unexpected character: 0x{:02x}", b),
);
self.skip_to_eol();
None
}
fn scan_identifier_or_keyword(&mut self) -> Token {
let start = self.pos;
let is_uppercase = self.source[self.pos].is_ascii_uppercase();
self.pos += 1;
loop {
match self.peek() {
Some(b) if b.is_ascii_alphanumeric() || b == b'_' => {
self.advance();
}
Some(b'-') => {
if self.is_comment_start() {
break;
}
self.advance();
}
_ => break,
}
}
let text = std::str::from_utf8(&self.source[start..self.pos])
.expect("identifier bytes should be ASCII");
if let Some(kind) = lookup_keyword(text) {
match kind {
TokenKind::KwMacro => {
self.state = LexerState::InMacro;
}
TokenKind::KwExports => {
self.state = LexerState::InExports;
}
_ => {}
}
return self.token(kind, start);
}
if is_forbidden_keyword(text) {
return self.token(TokenKind::ForbiddenKeyword, start);
}
let kind = if is_uppercase {
TokenKind::UppercaseIdent
} else {
TokenKind::LowercaseIdent
};
self.token(kind, start)
}
fn scan_digits(&mut self) {
while self.peek().is_some_and(|b| b.is_ascii_digit()) {
self.advance();
}
}
fn scan_number(&mut self) -> Token {
self.scan_number_impl(false)
}
fn scan_negative_number(&mut self) -> Token {
self.scan_number_impl(true)
}
fn scan_number_impl(&mut self, negative: bool) -> Token {
let start = self.pos;
if negative {
self.advance(); }
let digit_start = self.pos;
self.scan_digits();
let kind = if negative {
TokenKind::NegativeNumber
} else {
TokenKind::Number
};
let tok = self.token(kind, start);
if self.pos - digit_start > 1 && self.source[digit_start] == b'0' {
self.emit_diagnostic(
DiagCode::NumberLeadingZero,
self.span_from(start),
"leading zero(s) on a number",
);
}
tok
}
fn scan_quoted_string(&mut self) -> Token {
let start = self.pos;
self.advance();
loop {
match self.peek() {
None => {
let span = self.span_from(start);
self.emit_diagnostic(
DiagCode::UnterminatedString,
span,
"unterminated string literal",
);
return self.token(TokenKind::QuotedString, start);
}
Some(b'"') => {
self.advance();
return self.token(TokenKind::QuotedString, start);
}
_ => {
self.advance();
}
}
}
}
fn scan_hex_or_bin_string(&mut self) -> Token {
let start = self.pos;
self.advance();
let mut content_len = 0usize;
loop {
match self.peek() {
None | Some(b'\'') => break,
Some(b) => {
if !matches!(b, b' ' | b'\t' | b'\n' | b'\r') {
content_len += 1;
}
self.advance();
}
}
}
if self.peek() != Some(b'\'') {
let span = self.span_from(start);
self.emit_diagnostic(
DiagCode::UnterminatedHexBinStr,
span,
"unterminated hex/binary string",
);
return self.token(TokenKind::Error, start);
}
self.advance();
let suffix = self.peek();
match suffix {
Some(b'H' | b'h') => {
self.advance();
if content_len > 0 && !content_len.is_multiple_of(2) {
let span = self.span_from(start);
self.emit_diagnostic(
DiagCode::HexStringMul2,
span,
format!("hex string length {} is not a multiple of 2", content_len),
);
}
self.token(TokenKind::HexString, start)
}
Some(b'B' | b'b') => {
self.advance();
if content_len > 0 && !content_len.is_multiple_of(8) {
let span = self.span_from(start);
self.emit_diagnostic(
DiagCode::BinStringMul8,
span,
format!(
"binary string length {} is not a multiple of 8",
content_len
),
);
}
self.token(TokenKind::BinString, start)
}
_ => {
let span = self.span_from(start);
self.emit_diagnostic(
DiagCode::MissingHexBinSuffix,
span,
"expected 'H' or 'B' suffix for hex/binary string",
);
self.token(TokenKind::Error, start)
}
}
}
fn try_consume_triple_dash_eol(&mut self) -> bool {
if !self.remaining().starts_with(b"---") {
return false;
}
match self.source.get(self.pos + 3) {
None | Some(b'\n') | Some(b'\r') => {
self.pos += 3;
self.skip_line_ending();
true
}
_ => false,
}
}
fn emit_comment(&mut self) -> Token {
self.skip_comment_body(true);
let tok = self.token(TokenKind::Comment, self.comment_start);
if let Some(b) = self.peek()
&& (b == b'\n' || b == b'\r')
{
self.skip_line_ending();
}
self.state = LexerState::Normal;
tok
}
fn skip_comment_body(&mut self, handle_triple_dash: bool) {
loop {
match self.peek() {
None | Some(b'\n') | Some(b'\r') => return,
Some(b'-') => {
if handle_triple_dash && self.try_consume_triple_dash_eol() {
return;
}
if self.is_comment_start() {
self.pos += 2;
return;
}
self.advance();
}
_ => {
self.advance();
}
}
}
}
fn skip_comment_inline(&mut self) {
self.pos += 2; self.skip_comment_body(false);
}
fn skip_macro_body(&mut self) -> Token {
loop {
self.skip_whitespace();
if self.is_eof() {
let start = self.pos;
self.state = LexerState::Normal;
return self.token(TokenKind::Eof, start);
}
if self.remaining().starts_with(b"END") {
let prev_is_delimiter = self.pos == 0
|| (!self.source[self.pos - 1].is_ascii_alphanumeric()
&& self.source[self.pos - 1] != b'-');
if prev_is_delimiter {
let saved = self.pos;
self.pos += 3;
let is_delimiter = match self.peek() {
None => true,
Some(b'-') => self.peek_at(1) == Some(b'-'),
Some(b) => !b.is_ascii_alphanumeric() && b != b'-',
};
if is_delimiter {
self.state = LexerState::Normal;
return self.token(TokenKind::KwEnd, saved);
}
self.pos = saved;
}
}
if self.is_comment_start() {
self.skip_comment_inline();
continue;
}
self.advance();
}
}
fn skip_exports_body(&mut self) -> Token {
loop {
match self.peek() {
None => {
let start = self.pos;
self.state = LexerState::Normal;
return self.token(TokenKind::Eof, start);
}
Some(b';') => {
let start = self.pos;
self.advance();
self.state = LexerState::Normal;
return self.token(TokenKind::Semicolon, start);
}
_ => {
self.advance();
}
}
}
}
pub fn diagnostics(&self) -> &[SpanDiagnostic] {
&self.diagnostics
}
}
impl<'src, 'cfg> Iterator for Lexer<'src, 'cfg> {
type Item = Token;
fn next(&mut self) -> Option<Token> {
let tok = self.next_token();
if tok.kind == TokenKind::Eof {
None
} else {
Some(tok)
}
}
}
fn punctuation_kind(b: u8) -> Option<TokenKind> {
match b {
b'[' => Some(TokenKind::LBracket),
b']' => Some(TokenKind::RBracket),
b'{' => Some(TokenKind::LBrace),
b'}' => Some(TokenKind::RBrace),
b'(' => Some(TokenKind::LParen),
b')' => Some(TokenKind::RParen),
b';' => Some(TokenKind::Semicolon),
b',' => Some(TokenKind::Comma),
b'|' => Some(TokenKind::Pipe),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tokenize(input: &str) -> Vec<Token> {
let cfg = DiagnosticConfig::default();
let lexer = Lexer::new(input.as_bytes(), &cfg);
let (tokens, _) = lexer.tokenize();
tokens
}
fn tokenize_with_diags(input: &str) -> (Vec<Token>, Vec<SpanDiagnostic>) {
let cfg = DiagnosticConfig::verbose();
let lexer = Lexer::new(input.as_bytes(), &cfg);
lexer.tokenize()
}
fn kinds(tokens: &[Token]) -> Vec<TokenKind> {
tokens.iter().map(|t| t.kind).collect()
}
fn text_of<'a>(source: &'a str, token: &Token) -> &'a str {
&source[token.span.start.0 as usize..token.span.end.0 as usize]
}
#[test]
fn empty_input() {
let tokens = tokenize("");
assert_eq!(kinds(&tokens), vec![TokenKind::Eof]);
}
#[test]
fn whitespace_only() {
let tokens = tokenize(" \t\n\r\n ");
assert_eq!(kinds(&tokens), vec![TokenKind::Eof]);
}
#[test]
fn punctuation() {
let tokens = tokenize("[ ] { } ( ) ; , |");
assert_eq!(
kinds(&tokens),
vec![
TokenKind::LBracket,
TokenKind::RBracket,
TokenKind::LBrace,
TokenKind::RBrace,
TokenKind::LParen,
TokenKind::RParen,
TokenKind::Semicolon,
TokenKind::Comma,
TokenKind::Pipe,
TokenKind::Eof,
]
);
}
#[test]
fn dots_and_colons() {
let tokens = tokenize(". .. : ::=");
assert_eq!(
kinds(&tokens),
vec![
TokenKind::Dot,
TokenKind::DotDot,
TokenKind::Colon,
TokenKind::ColonColonEqual,
TokenKind::Eof,
]
);
}
#[test]
fn numbers() {
let input = "0 42 100";
let tokens = tokenize(input);
assert_eq!(
kinds(&tokens),
vec![
TokenKind::Number,
TokenKind::Number,
TokenKind::Number,
TokenKind::Eof,
]
);
assert_eq!(text_of(input, &tokens[0]), "0");
assert_eq!(text_of(input, &tokens[1]), "42");
assert_eq!(text_of(input, &tokens[2]), "100");
}
#[test]
fn negative_numbers() {
let input = "-1 -42";
let tokens = tokenize(input);
assert_eq!(
kinds(&tokens),
vec![
TokenKind::NegativeNumber,
TokenKind::NegativeNumber,
TokenKind::Eof,
]
);
assert_eq!(text_of(input, &tokens[0]), "-1");
assert_eq!(text_of(input, &tokens[1]), "-42");
}
#[test]
fn minus_not_negative() {
let tokens = tokenize("- x");
assert_eq!(
kinds(&tokens),
vec![TokenKind::Minus, TokenKind::LowercaseIdent, TokenKind::Eof]
);
}
#[test]
fn leading_zero_diagnostic() {
let (tokens, diags) = tokenize_with_diags("007 -042");
assert_eq!(
kinds(&tokens),
vec![TokenKind::Number, TokenKind::NegativeNumber, TokenKind::Eof,]
);
assert_eq!(diags.len(), 2);
assert_eq!(diags[0].code, DiagCode::NumberLeadingZero);
assert_eq!(diags[1].code, DiagCode::NumberLeadingZero);
}
#[test]
fn quoted_string() {
let input = r#""hello world""#;
let tokens = tokenize(input);
assert_eq!(
kinds(&tokens),
vec![TokenKind::QuotedString, TokenKind::Eof]
);
assert_eq!(text_of(input, &tokens[0]), r#""hello world""#);
}
#[test]
fn multiline_quoted_string() {
let input = "\"line1\nline2\"";
let tokens = tokenize(input);
assert_eq!(
kinds(&tokens),
vec![TokenKind::QuotedString, TokenKind::Eof]
);
}
#[test]
fn unterminated_string() {
let (tokens, diags) = tokenize_with_diags("\"hello");
assert_eq!(
kinds(&tokens),
vec![TokenKind::QuotedString, TokenKind::Eof]
);
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].code, DiagCode::UnterminatedString);
}
#[test]
fn hex_string() {
let input = "'0A1B'H";
let tokens = tokenize(input);
assert_eq!(kinds(&tokens), vec![TokenKind::HexString, TokenKind::Eof]);
}
#[test]
fn bin_string() {
let input = "'01010101'B";
let tokens = tokenize(input);
assert_eq!(kinds(&tokens), vec![TokenKind::BinString, TokenKind::Eof]);
}
#[test]
fn hex_string_odd_length() {
let (tokens, diags) = tokenize_with_diags("'0A1'H");
assert_eq!(kinds(&tokens), vec![TokenKind::HexString, TokenKind::Eof]);
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].code, DiagCode::HexStringMul2);
}
#[test]
fn bin_string_bad_length() {
let (tokens, diags) = tokenize_with_diags("'0101'B");
assert_eq!(kinds(&tokens), vec![TokenKind::BinString, TokenKind::Eof]);
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].code, DiagCode::BinStringMul8);
}
#[test]
fn hex_string_missing_suffix() {
let (tokens, diags) = tokenize_with_diags("'0A1B'");
assert_eq!(kinds(&tokens), vec![TokenKind::Error, TokenKind::Eof]);
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].code, DiagCode::MissingHexBinSuffix);
}
#[test]
fn unterminated_hex_string() {
let (tokens, diags) = tokenize_with_diags("'0A1B");
assert_eq!(kinds(&tokens), vec![TokenKind::Error, TokenKind::Eof]);
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].code, DiagCode::UnterminatedHexBinStr);
}
#[test]
fn identifiers() {
let input = "ifIndex SomeModule myVar";
let tokens = tokenize(input);
assert_eq!(
kinds(&tokens),
vec![
TokenKind::LowercaseIdent,
TokenKind::UppercaseIdent,
TokenKind::LowercaseIdent,
TokenKind::Eof,
]
);
assert_eq!(text_of(input, &tokens[0]), "ifIndex");
assert_eq!(text_of(input, &tokens[1]), "SomeModule");
assert_eq!(text_of(input, &tokens[2]), "myVar");
}
#[test]
fn identifier_with_hyphen() {
let input = "some-name";
let tokens = tokenize(input);
assert_eq!(
kinds(&tokens),
vec![TokenKind::LowercaseIdent, TokenKind::Eof]
);
assert_eq!(text_of(input, &tokens[0]), "some-name");
}
#[test]
fn identifier_with_underscore() {
let input = "some_name";
let tokens = tokenize(input);
assert_eq!(
kinds(&tokens),
vec![TokenKind::LowercaseIdent, TokenKind::Eof]
);
assert_eq!(text_of(input, &tokens[0]), "some_name");
}
#[test]
fn identifier_stops_at_comment() {
let input = "name--comment";
let tokens = tokenize(input);
assert_eq!(
kinds(&tokens),
vec![
TokenKind::LowercaseIdent,
TokenKind::Comment,
TokenKind::Eof,
]
);
assert_eq!(text_of(input, &tokens[0]), "name");
}
#[test]
fn keywords() {
let input = "OBJECT-TYPE SYNTAX STATUS DESCRIPTION";
let tokens = tokenize(input);
assert_eq!(
kinds(&tokens),
vec![
TokenKind::KwObjectType,
TokenKind::KwSyntax,
TokenKind::KwStatus,
TokenKind::KwDescription,
TokenKind::Eof,
]
);
}
#[test]
fn status_access_keywords() {
let input = "current deprecated read-only not-accessible";
let tokens = tokenize(input);
assert_eq!(
kinds(&tokens),
vec![
TokenKind::KwCurrent,
TokenKind::KwDeprecated,
TokenKind::KwReadOnly,
TokenKind::KwNotAccessible,
TokenKind::Eof,
]
);
}
#[test]
fn forbidden_keyword() {
let input = "TRUE FALSE NULL OPTIONAL";
let tokens = tokenize(input);
assert_eq!(
kinds(&tokens),
vec![
TokenKind::ForbiddenKeyword,
TokenKind::ForbiddenKeyword,
TokenKind::ForbiddenKeyword,
TokenKind::ForbiddenKeyword,
TokenKind::Eof,
]
);
}
#[test]
fn comment_to_eol() {
let input = "x -- this is a comment\ny";
let tokens = tokenize(input);
assert_eq!(
kinds(&tokens),
vec![
TokenKind::LowercaseIdent,
TokenKind::Comment,
TokenKind::LowercaseIdent,
TokenKind::Eof,
]
);
}
#[test]
fn comment_terminated_by_double_dash() {
let input = "x -- comment -- y";
let tokens = tokenize(input);
assert_eq!(
kinds(&tokens),
vec![
TokenKind::LowercaseIdent,
TokenKind::Comment,
TokenKind::LowercaseIdent,
TokenKind::Eof,
]
);
assert_eq!(text_of(input, &tokens[1]), "-- comment --");
}
#[test]
fn comment_at_eof() {
let input = "x -- comment";
let tokens = tokenize(input);
assert_eq!(
kinds(&tokens),
vec![
TokenKind::LowercaseIdent,
TokenKind::Comment,
TokenKind::Eof,
]
);
}
#[test]
fn triple_dash_eol_terminates_comment() {
let input = "-- comment---\nx";
let tokens = tokenize(input);
assert_eq!(
kinds(&tokens),
vec![
TokenKind::Comment,
TokenKind::LowercaseIdent,
TokenKind::Eof,
]
);
}
#[test]
fn macro_body_skipping() {
let input = "MACRO ::= BEGIN stuff END next";
let tokens = tokenize(input);
assert_eq!(
kinds(&tokens),
vec![
TokenKind::KwMacro,
TokenKind::KwEnd,
TokenKind::LowercaseIdent,
TokenKind::Eof,
]
);
}
#[test]
fn macro_body_does_not_match_pretend() {
let input = "MACRO ::= BEGIN PRETEND END next";
let tokens = tokenize(input);
let end_idx = tokens
.iter()
.position(|t| t.kind == TokenKind::KwEnd)
.unwrap();
let next_idx = tokens
.iter()
.position(|t| t.kind == TokenKind::LowercaseIdent)
.unwrap();
assert!(end_idx < next_idx);
}
#[test]
fn exports_body_skipping() {
let input = "EXPORTS foo, bar; next";
let tokens = tokenize(input);
assert_eq!(
kinds(&tokens),
vec![
TokenKind::KwExports,
TokenKind::Semicolon,
TokenKind::LowercaseIdent,
TokenKind::Eof,
]
);
}
#[test]
fn unexpected_character() {
let (tokens, diags) = tokenize_with_diags("x @ y");
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].code, DiagCode::UnexpectedCharacter);
assert_eq!(tokens[0].kind, TokenKind::LowercaseIdent);
}
#[test]
fn realistic_snippet() {
let input = r#"IF-MIB DEFINITIONS ::= BEGIN
IMPORTS
MODULE-IDENTITY, OBJECT-TYPE, Integer32
FROM SNMPv2-SMI;
ifMIB MODULE-IDENTITY
LAST-UPDATED "200006140000Z"
ORGANIZATION "IETF"
CONTACT-INFO ""
DESCRIPTION
"The MIB module to describe interfaces."
::= { mib-2 31 }
END
"#;
let tokens = tokenize(input);
assert_eq!(tokens.last().unwrap().kind, TokenKind::Eof);
let k = kinds(&tokens);
assert!(k.contains(&TokenKind::KwDefinitions));
assert!(k.contains(&TokenKind::ColonColonEqual));
assert!(k.contains(&TokenKind::KwBegin));
assert!(k.contains(&TokenKind::KwImports));
assert!(k.contains(&TokenKind::KwModuleIdentity));
assert!(k.contains(&TokenKind::KwEnd));
}
#[test]
fn hex_string_with_whitespace() {
let input = "'0A 1B\n2C'H";
let tokens = tokenize(input);
assert_eq!(kinds(&tokens), vec![TokenKind::HexString, TokenKind::Eof]);
}
#[test]
fn empty_hex_string() {
let input = "''H";
let tokens = tokenize(input);
assert_eq!(kinds(&tokens), vec![TokenKind::HexString, TokenKind::Eof]);
}
#[test]
fn type_keywords() {
let input = "INTEGER Counter32 IpAddress OCTET STRING BITS";
let tokens = tokenize(input);
assert_eq!(
kinds(&tokens),
vec![
TokenKind::KwInteger,
TokenKind::KwCounter32,
TokenKind::KwIpAddress,
TokenKind::KwOctet,
TokenKind::KwString,
TokenKind::KwBits,
TokenKind::Eof,
]
);
}
#[test]
fn colon_not_cce() {
let input = ": ::";
let tokens = tokenize(input);
assert_eq!(
kinds(&tokens),
vec![
TokenKind::Colon,
TokenKind::Colon,
TokenKind::Colon,
TokenKind::Eof,
]
);
}
#[test]
fn span_offsets_correct() {
let input = "abc 123";
let tokens = tokenize(input);
assert_eq!(tokens[0].span.start.0, 0);
assert_eq!(tokens[0].span.end.0, 3);
assert_eq!(tokens[1].span.start.0, 4);
assert_eq!(tokens[1].span.end.0, 7);
}
}