Files
MetaCrate/crates/libremetaverse-lsl-tools/src/lexer.rs
Chili Palmer 2558e2f49f
Some checks failed
Native code generation / deterministic (push) Failing after 2m36s
Imaging and meshing gate / native (push) Successful in 5m29s
JPEG 2000 feature / linux (push) Successful in 2m46s
Native Rust workspace compile / compile (push) Failing after 1m58s
Skia feature / linux (push) Successful in 31m14s
Generate native LSL parser tables (#83)
2026-08-11 03:36:14 +00:00

3172 lines
98 KiB
Rust

//! Native lexer runtime and source diagnostics.
//!
//! Positions in this module are UTF-16 code-unit offsets. That is deliberate:
//! the original C# API exposes `System.Char` positions, and using byte offsets
//! would move every diagnostic after a non-ASCII character.
#![allow(clippy::inherent_to_string_shadow_display)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]
#![allow(clippy::must_use_candidate)]
#![allow(clippy::needless_pass_by_value)]
#![allow(clippy::should_implement_trait)]
#![allow(clippy::too_many_lines)]
use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::fmt;
use std::io::Write;
use libremetaverse_types::compat::{
Hashtable, IEnumerator, Object, StreamReader, TextEncoding, UnicodeCategory, Utf16CodeUnit,
};
use unicode_general_category::{GeneralCategory, get_general_category};
use crate::{Error, Nfa, Parser, ParserEntry, Serialiser, SymbolsGen, TokensGen, YyParser};
/// Largest source accepted by the compatibility reader (64 Mi UTF-16 units).
pub const MAX_SOURCE_UNITS: usize = 64 * 1024 * 1024;
/// Largest individual token accepted by the lexer (16 Mi UTF-16 units).
pub const MAX_TOKEN_UNITS: usize = 16 * 1024 * 1024;
fn index(value: i32) -> Result<usize, Error> {
usize::try_from(value).map_err(|_| Error::IndexOutOfRange)
}
fn i32_len(value: usize) -> Result<i32, Error> {
i32::try_from(value).map_err(|_| Error::IndexOutOfRange)
}
fn units_to_string(units: &[u16]) -> String {
String::from_utf16_lossy(units)
}
fn utf16_len(value: &str) -> usize {
value.encode_utf16().count()
}
/// .NET-compatible Unicode general categories.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[repr(i32)]
pub enum DotNetUnicodeCategory {
UppercaseLetter = 0,
LowercaseLetter = 1,
TitlecaseLetter = 2,
ModifierLetter = 3,
OtherLetter = 4,
NonSpacingMark = 5,
SpacingCombiningMark = 6,
EnclosingMark = 7,
DecimalDigitNumber = 8,
LetterNumber = 9,
OtherNumber = 10,
SpaceSeparator = 11,
LineSeparator = 12,
ParagraphSeparator = 13,
Control = 14,
Format = 15,
Surrogate = 16,
PrivateUse = 17,
ConnectorPunctuation = 18,
DashPunctuation = 19,
OpenPunctuation = 20,
ClosePunctuation = 21,
InitialQuotePunctuation = 22,
FinalQuotePunctuation = 23,
OtherPunctuation = 24,
MathSymbol = 25,
CurrencySymbol = 26,
ModifierSymbol = 27,
OtherSymbol = 28,
OtherNotAssigned = 29,
}
impl DotNetUnicodeCategory {
fn from_compat(value: UnicodeCategory) -> Result<Self, Error> {
Self::try_from(value.0)
}
fn of_unit(unit: u16) -> Self {
let Some(ch) = char::from_u32(u32::from(unit)) else {
return Self::Surrogate;
};
match get_general_category(ch) {
GeneralCategory::UppercaseLetter => Self::UppercaseLetter,
GeneralCategory::LowercaseLetter => Self::LowercaseLetter,
GeneralCategory::TitlecaseLetter => Self::TitlecaseLetter,
GeneralCategory::ModifierLetter => Self::ModifierLetter,
GeneralCategory::OtherLetter => Self::OtherLetter,
GeneralCategory::NonspacingMark => Self::NonSpacingMark,
GeneralCategory::SpacingMark => Self::SpacingCombiningMark,
GeneralCategory::EnclosingMark => Self::EnclosingMark,
GeneralCategory::DecimalNumber => Self::DecimalDigitNumber,
GeneralCategory::LetterNumber => Self::LetterNumber,
GeneralCategory::OtherNumber => Self::OtherNumber,
GeneralCategory::SpaceSeparator => Self::SpaceSeparator,
GeneralCategory::LineSeparator => Self::LineSeparator,
GeneralCategory::ParagraphSeparator => Self::ParagraphSeparator,
GeneralCategory::Control => Self::Control,
GeneralCategory::Format => Self::Format,
GeneralCategory::Surrogate => Self::Surrogate,
GeneralCategory::PrivateUse => Self::PrivateUse,
GeneralCategory::ConnectorPunctuation => Self::ConnectorPunctuation,
GeneralCategory::DashPunctuation => Self::DashPunctuation,
GeneralCategory::OpenPunctuation => Self::OpenPunctuation,
GeneralCategory::ClosePunctuation => Self::ClosePunctuation,
GeneralCategory::InitialPunctuation => Self::InitialQuotePunctuation,
GeneralCategory::FinalPunctuation => Self::FinalQuotePunctuation,
GeneralCategory::OtherPunctuation => Self::OtherPunctuation,
GeneralCategory::MathSymbol => Self::MathSymbol,
GeneralCategory::CurrencySymbol => Self::CurrencySymbol,
GeneralCategory::ModifierSymbol => Self::ModifierSymbol,
GeneralCategory::OtherSymbol => Self::OtherSymbol,
_ => Self::OtherNotAssigned,
}
}
fn name(self) -> &'static str {
match self {
Self::UppercaseLetter => "UppercaseLetter",
Self::LowercaseLetter => "LowercaseLetter",
Self::TitlecaseLetter => "TitlecaseLetter",
Self::ModifierLetter => "ModifierLetter",
Self::OtherLetter => "OtherLetter",
Self::NonSpacingMark => "NonSpacingMark",
Self::SpacingCombiningMark => "SpacingCombiningMark",
Self::EnclosingMark => "EnclosingMark",
Self::DecimalDigitNumber => "DecimalDigitNumber",
Self::LetterNumber => "LetterNumber",
Self::OtherNumber => "OtherNumber",
Self::SpaceSeparator => "SpaceSeparator",
Self::LineSeparator => "LineSeparator",
Self::ParagraphSeparator => "ParagraphSeparator",
Self::Control => "Control",
Self::Format => "Format",
Self::Surrogate => "Surrogate",
Self::PrivateUse => "PrivateUse",
Self::ConnectorPunctuation => "ConnectorPunctuation",
Self::DashPunctuation => "DashPunctuation",
Self::OpenPunctuation => "OpenPunctuation",
Self::ClosePunctuation => "ClosePunctuation",
Self::InitialQuotePunctuation => "InitialQuotePunctuation",
Self::FinalQuotePunctuation => "FinalQuotePunctuation",
Self::OtherPunctuation => "OtherPunctuation",
Self::MathSymbol => "MathSymbol",
Self::CurrencySymbol => "CurrencySymbol",
Self::ModifierSymbol => "ModifierSymbol",
Self::OtherSymbol => "OtherSymbol",
Self::OtherNotAssigned => "OtherNotAssigned",
}
}
}
impl TryFrom<i32> for DotNetUnicodeCategory {
type Error = Error;
fn try_from(value: i32) -> Result<Self, Self::Error> {
Ok(match value {
0 => Self::UppercaseLetter,
1 => Self::LowercaseLetter,
2 => Self::TitlecaseLetter,
3 => Self::ModifierLetter,
4 => Self::OtherLetter,
5 => Self::NonSpacingMark,
6 => Self::SpacingCombiningMark,
7 => Self::EnclosingMark,
8 => Self::DecimalDigitNumber,
9 => Self::LetterNumber,
10 => Self::OtherNumber,
11 => Self::SpaceSeparator,
12 => Self::LineSeparator,
13 => Self::ParagraphSeparator,
14 => Self::Control,
15 => Self::Format,
16 => Self::Surrogate,
17 => Self::PrivateUse,
18 => Self::ConnectorPunctuation,
19 => Self::DashPunctuation,
20 => Self::OpenPunctuation,
21 => Self::ClosePunctuation,
22 => Self::InitialQuotePunctuation,
23 => Self::FinalQuotePunctuation,
24 => Self::OtherPunctuation,
25 => Self::MathSymbol,
26 => Self::CurrencySymbol,
27 => Self::ModifierSymbol,
28 => Self::OtherSymbol,
29 => Self::OtherNotAssigned,
_ => return Err(Error::Argument),
})
}
}
/// Character-category predicate corresponding to the C# `CatTest` delegate.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CatTest {
category: DotNetUnicodeCategory,
}
impl CatTest {
/// Creates a category predicate.
pub fn new(category: UnicodeCategory) -> Result<Self, Error> {
Ok(Self {
category: DotNetUnicodeCategory::from_compat(category)?,
})
}
/// Tests one UTF-16 code unit using .NET category numbering.
pub fn test(&self, ch: Utf16CodeUnit) -> Result<bool, Error> {
Ok(DotNetUnicodeCategory::of_unit(ch.0) == self.category)
}
}
/// Input encodings supported by lexer source files.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum InputEncoding {
Ascii,
AsciiUpper,
#[default]
Utf8,
Utf16Le,
Utf16Be,
}
impl InputEncoding {
/// Resolves the names accepted by the C# runtime.
pub fn parse(name: &str) -> Result<Self, Error> {
match name.trim().to_ascii_uppercase().as_str() {
"" | "UTF8" | "UTF-8" | "65001" => Ok(Self::Utf8),
"ASCII" | "US-ASCII" | "20127" => Ok(Self::Ascii),
"ASCIICAPS" => Ok(Self::AsciiUpper),
"UNICODE" | "UTF16" | "UTF-16" | "UTF-16LE" | "1200" => Ok(Self::Utf16Le),
"UTF-16BE" | "1201" => Ok(Self::Utf16Be),
// UTF-7 is intentionally rejected: it is obsolete and unsafe, and
// modern .NET also disables it unless explicitly enabled.
_ => Err(Error::Argument),
}
}
fn decode(self, bytes: &[u8]) -> Result<String, Error> {
let bytes = match bytes.strip_prefix(&[0xEF, 0xBB, 0xBF]) {
Some(rest) => rest,
None => bytes,
};
match self {
Self::Utf8 => String::from_utf8(bytes.to_vec()).map_err(|error| Error::Parse {
position: error.utf8_error().valid_up_to(),
context: "source is not valid UTF-8",
}),
Self::Ascii | Self::AsciiUpper => {
if let Some(position) = bytes.iter().position(|byte| !byte.is_ascii()) {
return Err(Error::Parse {
position,
context: "source contains a non-ASCII byte",
});
}
let value = String::from_utf8(bytes.to_vec()).map_err(|_| Error::Argument)?;
Ok(if self == Self::AsciiUpper {
value.to_ascii_uppercase()
} else {
value
})
}
Self::Utf16Le | Self::Utf16Be => {
let (encoding, bytes) = if let Some(bytes) = bytes.strip_prefix(&[0xFF, 0xFE]) {
(Self::Utf16Le, bytes)
} else if let Some(bytes) = bytes.strip_prefix(&[0xFE, 0xFF]) {
(Self::Utf16Be, bytes)
} else {
(self, bytes)
};
if bytes.len() % 2 != 0 {
return Err(Error::Parse {
position: bytes.len() - 1,
context: "UTF-16 source has a trailing byte",
});
}
let units = bytes
.chunks_exact(2)
.map(|pair| match encoding {
Self::Utf16Le => u16::from_le_bytes([pair[0], pair[1]]),
Self::Utf16Be => u16::from_be_bytes([pair[0], pair[1]]),
_ => unreachable!(),
})
.collect::<Vec<_>>();
String::from_utf16(&units).map_err(|_| Error::Parse {
position: first_unpaired_surrogate(&units),
context: "source contains an unpaired UTF-16 surrogate",
})
}
}
}
}
fn first_unpaired_surrogate(units: &[u16]) -> usize {
let mut index = 0;
while index < units.len() {
match units[index] {
0xD800..=0xDBFF
if units
.get(index + 1)
.is_some_and(|unit| (0xDC00..=0xDFFF).contains(unit)) =>
{
index += 2;
}
0xD800..=0xDFFF => return index,
_ => index += 1,
}
}
units.len()
}
/// Unicode character set used by generated lexer transitions.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Charset {
pub(crate) category: DotNetUnicodeCategory,
pub(crate) generic: u16,
pub(crate) explicit: BTreeSet<u16>,
}
impl Charset {
fn for_category(category: DotNetUnicodeCategory) -> Self {
let generic = (0..=u16::MAX)
.find(|unit| DotNetUnicodeCategory::of_unit(*unit) == category)
.unwrap_or(0);
Self {
category,
generic,
explicit: BTreeSet::from([generic]),
}
}
/// Resolves a source encoding and updates the C# uppercase compatibility flag.
pub fn get_encoding(
enc: String,
toupper: &mut bool,
mut erh: ErrorHandler,
) -> Result<TextEncoding, Error> {
if let Ok(encoding) = InputEncoding::parse(&enc) {
*toupper = encoding == InputEncoding::AsciiUpper;
Ok(TextEncoding)
} else {
let diagnostic = CSToolsException::new_with_int32_string(
43,
format!("Warning: Encoding {enc} unknown: ignored"),
)?;
erh.error(diagnostic)?;
*toupper = false;
Ok(TextEncoding)
}
}
/// Rejects the generator serializer boundary until its native table codec is
/// available. Lexer tables use [`YyLexer::emit_dfa`] in this runtime.
pub fn serialise(o: Object, _s: Serialiser) -> Result<Object, Error> {
let _ = o;
Err(Error::InvalidOperation)
}
/// Tests whether a UTF-16 unit belongs to this set.
#[must_use]
pub fn contains(&self, unit: u16) -> bool {
self.explicit.contains(&unit) || DotNetUnicodeCategory::of_unit(unit) == self.category
}
}
/// One removed comment segment, measured in UTF-16 code units.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CommentList {
pub spos: i32,
pub len: i32,
pub tail: Option<Box<CommentList>>,
}
impl CommentList {
pub fn new(st: i32, ln: i32, tail: CommentList) -> Result<Self, Error> {
if st < 0 || ln < 0 {
return Err(Error::Argument);
}
Ok(Self {
spos: st,
len: ln,
tail: Some(Box::new(tail)),
})
}
}
/// Linked-list compatibility view of a source line.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LineList {
pub head: i32,
pub comments: Option<Box<CommentList>>,
pub tail: Option<Box<LineList>>,
}
impl LineList {
pub fn new(head: i32, tail: LineList) -> Result<Self, Error> {
if head < 0 {
return Err(Error::Argument);
}
Ok(Self {
head,
comments: None,
tail: Some(Box::new(tail)),
})
}
fn first(head: i32) -> Self {
Self {
head,
comments: None,
tail: None,
}
}
pub fn getpos(&self, pos: i32) -> Result<i32, Error> {
if pos < self.head {
return Err(Error::IndexOutOfRange);
}
let mut result = pos - self.head;
let mut comment = self.comments.as_deref();
while let Some(item) = comment {
if pos > item.spos {
result = result.checked_add(item.len).ok_or(Error::IndexOutOfRange)?;
}
comment = item.tail.as_deref();
}
Ok(result)
}
}
/// Tracks filtered and raw line positions.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LineManager {
pub end: i32,
pub lines: i32,
pub list: Option<Box<LineList>>,
starts: Vec<i32>,
logical_lines: Vec<i32>,
}
impl Default for LineManager {
fn default() -> Self {
Self {
end: 0,
lines: 1,
list: None,
starts: vec![0],
logical_lines: vec![1],
}
}
}
impl LineManager {
pub fn new() -> Result<Self, Error> {
Ok(Self::default())
}
pub fn newline(&mut self, pos: i32) -> Result<(), Error> {
self.backto(pos)?;
self.lines = self.lines.checked_add(1).ok_or(Error::IndexOutOfRange)?;
self.starts.push(pos);
self.logical_lines.push(self.lines);
let previous = self.list.take();
self.list = Some(Box::new(LineList {
head: pos,
comments: None,
tail: previous,
}));
Ok(())
}
pub fn backto(&mut self, pos: i32) -> Result<(), Error> {
if pos < 0 {
return Err(Error::Argument);
}
self.end = self.end.max(pos);
while self.starts.len() > 1 && self.starts.last().is_some_and(|start| *start >= pos) {
self.starts.pop();
self.logical_lines.pop();
self.lines -= 1;
self.list = self.list.take().and_then(|line| line.tail);
}
Ok(())
}
pub fn comment(&mut self, pos: i32, len: i32) -> Result<(), Error> {
if pos < 0 || len < 0 {
return Err(Error::Argument);
}
self.end = self.end.max(pos);
if self.list.is_none() {
self.list = Some(Box::new(LineList::first(0)));
self.lines = 1;
}
if let Some(line) = self.list.as_mut() {
let tail = line.comments.take();
line.comments = Some(Box::new(CommentList {
spos: pos,
len,
tail,
}));
}
Ok(())
}
fn set_logical_line(&mut self, line: i32) -> Result<(), Error> {
if line < 1 {
return Err(Error::Argument);
}
self.lines = line - 1;
if let Some(last) = self.logical_lines.last_mut() {
*last = line - 1;
}
Ok(())
}
fn locate(&self, pos: i32) -> Result<(usize, i32, i32, i32), Error> {
if pos < 0 || pos > self.end {
return Err(Error::IndexOutOfRange);
}
let line_index = self
.starts
.partition_point(|start| *start <= pos)
.saturating_sub(1);
let start = self.starts[line_index];
let end = self.starts.get(line_index + 1).copied().unwrap_or(self.end);
let line = self
.logical_lines
.get(line_index)
.copied()
.unwrap_or_else(|| i32::try_from(line_index + 1).unwrap_or(i32::MAX));
Ok((line_index, line, start, end))
}
}
/// Source location compatible with `SourceLineInfo`.
#[derive(Clone, Debug, Default)]
pub struct SourceLineInfo {
pub char_position: i32,
pub end_of_line: i32,
pub line_number: i32,
pub lxr: Option<Box<Lexer>>,
pub raw_char_position: i32,
pub start_of_line: i32,
source_snapshot: String,
}
impl SourceLineInfo {
pub fn new_with_int32(pos: i32) -> Result<Self, Error> {
if pos < 0 {
return Err(Error::Argument);
}
Ok(Self {
char_position: 1,
end_of_line: pos,
line_number: 1,
lxr: None,
raw_char_position: pos,
start_of_line: 0,
source_snapshot: String::new(),
})
}
pub fn new_with_line_manager_int32(manager: LineManager, pos: i32) -> Result<Self, Error> {
Self::from_manager(&manager, pos, String::new())
}
pub fn new_with_lexer_int32(lexer: Lexer, pos: i32) -> Result<Self, Error> {
Self::from_manager(&lexer.m_line_manager, pos, lexer.m_buf.clone())
}
fn from_manager(manager: &LineManager, pos: i32, source: String) -> Result<Self, Error> {
let (line_index, line_number, start, end) = manager.locate(pos)?;
let mut raw = pos - start + i32::from(line_index > 0);
let mut line = manager.list.as_deref();
while let Some(candidate) = line {
if candidate.head == start {
raw = candidate
.getpos(pos)?
.checked_add(i32::from(line_index > 0))
.ok_or(Error::IndexOutOfRange)?;
break;
}
line = candidate.tail.as_deref();
}
Ok(Self {
char_position: pos - start + 1,
end_of_line: end,
line_number,
lxr: None,
raw_char_position: raw,
start_of_line: start,
source_snapshot: source,
})
}
#[must_use]
pub fn source_line(&self) -> String {
if self.source_snapshot.is_empty() {
return String::new();
}
let units = self.source_snapshot.encode_utf16().collect::<Vec<_>>();
let start = usize::try_from(self.start_of_line)
.unwrap_or(0)
.min(units.len());
let end = usize::try_from(self.end_of_line)
.unwrap_or(units.len())
.min(units.len());
units_to_string(&units[start..end])
.trim_end_matches('\n')
.to_owned()
}
#[must_use]
pub fn to_string(&self) -> String {
format!("Line {}, char {}", self.line_number, self.raw_char_position)
}
}
impl fmt::Display for SourceLineInfo {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.to_string())
}
}
/// Stable diagnostic categories emitted by the lexer runtime.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DiagnosticCategory {
Encoding,
InvalidCharacter,
InvalidState,
ParserRecovery,
ParserStackLimit,
Source,
Syntax,
TokenTooLong,
UnknownCharacterSet,
UnexpectedEof,
}
/// Diagnostic severity.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DiagnosticSeverity {
Warning,
Error,
Fatal,
Stop,
}
/// Structured diagnostic suitable for deterministic fixture assertions.
#[derive(Clone, Debug)]
pub struct Diagnostic {
pub code: i32,
pub category: DiagnosticCategory,
pub severity: DiagnosticSeverity,
pub message: String,
pub location: SourceLineInfo,
pub input: String,
}
/// C# compatibility exception carrying a structured native diagnostic.
#[derive(Debug)]
pub struct CSToolsException {
pub handled: bool,
pub n_exception_number: i32,
pub s_input: String,
pub sl_info: SourceLineInfo,
pub sym: Option<Box<SYMBOL>>,
pub message: String,
severity: DiagnosticSeverity,
category: DiagnosticCategory,
}
impl Clone for CSToolsException {
fn clone(&self) -> Self {
Self {
handled: self.handled,
n_exception_number: self.n_exception_number,
s_input: self.s_input.clone(),
sl_info: self.sl_info.clone(),
sym: None,
message: self.message.clone(),
severity: self.severity,
category: self.category,
}
}
}
impl CSToolsException {
fn category_for_code(number: i32) -> DiagnosticCategory {
match number {
2 => DiagnosticCategory::InvalidState,
24 => DiagnosticCategory::UnknownCharacterSet,
43 => DiagnosticCategory::Encoding,
_ => DiagnosticCategory::Source,
}
}
fn create(
number: i32,
location: SourceLineInfo,
input: String,
message: String,
severity: DiagnosticSeverity,
category: DiagnosticCategory,
) -> Self {
Self {
handled: false,
n_exception_number: number,
s_input: input,
sl_info: location,
sym: None,
message,
severity,
category,
}
}
pub fn new_with_int32_string(number: i32, message: String) -> Result<Self, Error> {
Ok(Self::create(
number,
SourceLineInfo::new_with_int32(0)?,
String::new(),
message,
DiagnosticSeverity::Error,
Self::category_for_code(number),
))
}
pub fn new_with_int32_source_line_info_string_string(
number: i32,
location: SourceLineInfo,
input: String,
message: String,
) -> Result<Self, Error> {
Ok(Self::create(
number,
location,
input,
message,
DiagnosticSeverity::Error,
Self::category_for_code(number),
))
}
pub fn new_with_int32_lexer_int32_string_string(
number: i32,
lexer: Lexer,
pos: i32,
input: String,
message: String,
) -> Result<Self, Error> {
let location = lexer.source_line_info(pos)?;
Ok(Self::create(
number,
location,
input,
message,
DiagnosticSeverity::Error,
Self::category_for_code(number),
))
}
pub fn new_with_int32_lexer_string(
number: i32,
lexer: Lexer,
message: String,
) -> Result<Self, Error> {
let pos = lexer.yypos();
Self::new_with_int32_lexer_int32_string_string(number, lexer, pos, String::new(), message)
}
pub fn new_with_int32_lexer_string_string(
number: i32,
lexer: Lexer,
input: String,
message: String,
) -> Result<Self, Error> {
let pos = lexer.yypos();
Self::new_with_int32_lexer_int32_string_string(number, lexer, pos, input, message)
}
pub fn new_with_int32_symbol_string(
number: i32,
symbol: SYMBOL,
message: String,
) -> Result<Self, Error> {
let location = symbol.location()?;
let mut result = Self::create(
number,
location,
symbol.yyname(),
message,
DiagnosticSeverity::Error,
Self::category_for_code(number),
);
result.sym = Some(Box::new(symbol));
Ok(result)
}
pub fn new_with_int32_token_string(
number: i32,
token: TOKEN,
message: String,
) -> Result<Self, Error> {
Ok(Self::create(
number,
token.location()?,
token.yytext(),
message,
DiagnosticSeverity::Error,
Self::category_for_code(number),
))
}
pub fn handle(&mut self, handler: ErrorHandler) -> Result<(), Error> {
if handler.throw_exceptions {
return Err(Error::InvalidOperation);
}
self.handled = true;
Ok(())
}
fn diagnostic(&self) -> Diagnostic {
Diagnostic {
code: self.n_exception_number,
category: self.category,
severity: self.severity,
message: self.message.clone(),
location: self.sl_info.clone(),
input: self.s_input.clone(),
}
}
}
impl fmt::Display for CSToolsException {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}: {}", self.sl_info, self.message)
}
}
impl std::error::Error for CSToolsException {}
macro_rules! exception_wrapper {
($name:ident, $severity:expr) => {
#[derive(Clone, Debug)]
pub struct $name(pub CSToolsException);
impl $name {
pub fn new_with_int32_string(number: i32, message: String) -> Result<Self, Error> {
let mut inner = CSToolsException::new_with_int32_string(number, message)?;
inner.severity = $severity;
Ok(Self(inner))
}
pub fn new_with_int32_source_line_info_string_string(
number: i32,
location: SourceLineInfo,
input: String,
message: String,
) -> Result<Self, Error> {
let mut inner = CSToolsException::new_with_int32_source_line_info_string_string(
number, location, input, message,
)?;
inner.severity = $severity;
Ok(Self(inner))
}
pub fn new_with_int32_lexer_int32_string_string(
number: i32,
lexer: Lexer,
pos: i32,
input: String,
message: String,
) -> Result<Self, Error> {
let mut inner = CSToolsException::new_with_int32_lexer_int32_string_string(
number, lexer, pos, input, message,
)?;
inner.severity = $severity;
Ok(Self(inner))
}
pub fn new_with_int32_lexer_string(
number: i32,
lexer: Lexer,
message: String,
) -> Result<Self, Error> {
let mut inner =
CSToolsException::new_with_int32_lexer_string(number, lexer, message)?;
inner.severity = $severity;
Ok(Self(inner))
}
pub fn new_with_int32_lexer_string_string(
number: i32,
lexer: Lexer,
input: String,
message: String,
) -> Result<Self, Error> {
let mut inner = CSToolsException::new_with_int32_lexer_string_string(
number, lexer, input, message,
)?;
inner.severity = $severity;
Ok(Self(inner))
}
pub fn new_with_int32_token_string(
number: i32,
token: TOKEN,
message: String,
) -> Result<Self, Error> {
let mut inner =
CSToolsException::new_with_int32_token_string(number, token, message)?;
inner.severity = $severity;
Ok(Self(inner))
}
pub fn new_with_int32_symbol_string(
number: i32,
symbol: SYMBOL,
message: String,
) -> Result<Self, Error> {
let mut inner =
CSToolsException::new_with_int32_symbol_string(number, symbol, message)?;
inner.severity = $severity;
Ok(Self(inner))
}
pub fn handle(&mut self, _handler: ErrorHandler) -> Result<(), Error> {
Err(Error::InvalidOperation)
}
}
};
}
exception_wrapper!(CSToolsFatalException, DiagnosticSeverity::Fatal);
exception_wrapper!(CSToolsStopException, DiagnosticSeverity::Stop);
/// Collecting error handler. No output is written implicitly.
#[derive(Clone, Debug, Default)]
pub struct ErrorHandler {
pub counter: i32,
pub throw_exceptions: bool,
diagnostics: Vec<Diagnostic>,
}
impl ErrorHandler {
pub fn new_with_constructor() -> Result<Self, Error> {
Ok(Self::default())
}
pub fn new_with_boolean(throw_exceptions: bool) -> Result<Self, Error> {
Ok(Self {
throw_exceptions,
..Self::default()
})
}
pub fn error(&mut self, mut error: CSToolsException) -> Result<(), Error> {
self.counter = self.counter.checked_add(1).ok_or(Error::IndexOutOfRange)?;
if self.throw_exceptions {
return Err(Error::InvalidOperation);
}
self.report(error.clone())?;
error.handled = true;
Ok(())
}
pub fn report(&mut self, error: CSToolsException) -> Result<(), Error> {
self.diagnostics.push(error.diagnostic());
Ok(())
}
#[must_use]
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
pub fn take_diagnostics(&mut self) -> Vec<Diagnostic> {
std::mem::take(&mut self.diagnostics)
}
pub(crate) fn push(&mut self, diagnostic: Diagnostic) -> Result<(), Error> {
self.counter = self.counter.checked_add(1).ok_or(Error::IndexOutOfRange)?;
if self.throw_exceptions {
return Err(Error::InvalidOperation);
}
self.diagnostics.push(diagnostic);
Ok(())
}
}
/// Comment-filtering source reader corresponding to the C# `CsReader`.
#[derive(Clone, Debug)]
pub struct CsReader {
pub fname: String,
pub lm: LineManager,
units: Vec<u16>,
cursor: usize,
}
impl CsReader {
pub fn new_with_string(data: String) -> Result<Self, Error> {
Self::from_source(data, String::new())
}
pub fn new_with_string_encoding(
file_name: String,
_encoding: TextEncoding,
) -> Result<Self, Error> {
let bytes = std::fs::read(&file_name).map_err(|_| Error::Argument)?;
let encoding = if bytes.starts_with(&[0xFF, 0xFE]) {
InputEncoding::Utf16Le
} else if bytes.starts_with(&[0xFE, 0xFF]) {
InputEncoding::Utf16Be
} else {
InputEncoding::Utf8
};
Self::from_source(encoding.decode(&bytes)?, file_name)
}
pub fn new_with_cs_reader_encoding(
reader: CsReader,
_encoding: TextEncoding,
) -> Result<Self, Error> {
Ok(reader)
}
/// Creates a reader from encoded bytes without a platform-specific API.
pub fn from_bytes(
bytes: &[u8],
encoding: InputEncoding,
file_name: impl Into<String>,
) -> Result<Self, Error> {
Self::from_source(encoding.decode(bytes)?, file_name.into())
}
fn from_source(source: String, file_name: String) -> Result<Self, Error> {
let input = source.encode_utf16().collect::<Vec<_>>();
if input.len() > MAX_SOURCE_UNITS {
return Err(Error::Argument);
}
let (units, lm, directive_name) = filter_source(&input)?;
Ok(Self {
fname: directive_name.unwrap_or(file_name),
lm,
units,
cursor: 0,
})
}
pub fn eof(&self) -> Result<bool, Error> {
Ok(self.cursor >= self.units.len())
}
pub fn read_with_method(&mut self) -> Result<i32, Error> {
let Some(unit) = self.units.get(self.cursor).copied() else {
return Ok(-1);
};
self.cursor += 1;
Ok(i32::from(unit))
}
pub fn read_with_char_array_int32_int32(
&mut self,
mut destination: Vec<Utf16CodeUnit>,
offset: i32,
count: i32,
) -> Result<i32, Error> {
let offset = index(offset)?;
let count = index(count)?;
let end = offset.checked_add(count).ok_or(Error::IndexOutOfRange)?;
if end > destination.len() {
return Err(Error::IndexOutOfRange);
}
let copied = self.read_into(&mut destination[offset..end]);
i32_len(copied)
}
pub fn read_into(&mut self, destination: &mut [Utf16CodeUnit]) -> usize {
let count = destination
.len()
.min(self.units.len().saturating_sub(self.cursor));
for (slot, unit) in destination
.iter_mut()
.zip(&self.units[self.cursor..self.cursor + count])
{
*slot = Utf16CodeUnit(*unit);
}
self.cursor += count;
count
}
pub fn read_line(&mut self) -> Result<String, Error> {
if self.cursor >= self.units.len() {
return Ok(String::new());
}
let start = self.cursor;
while self.cursor < self.units.len() && self.units[self.cursor] != b'\n'.into() {
self.cursor += 1;
}
let line = units_to_string(&self.units[start..self.cursor]);
if self.cursor < self.units.len() {
self.cursor += 1;
}
Ok(line)
}
fn into_parts(self) -> (String, Vec<u16>, LineManager) {
(self.fname, self.units, self.lm)
}
}
fn filter_source(input: &[u16]) -> Result<(Vec<u16>, LineManager, Option<String>), Error> {
#[derive(Clone, Copy)]
enum State {
Normal,
Slash,
LineComment { removed: usize },
BlockComment { removed: usize },
BlockStar { removed: usize },
}
let mut output = Vec::with_capacity(input.len());
let mut lm = LineManager::default();
let mut state = State::Normal;
let mut at_line_start = true;
let mut directive_name = None;
let mut i = 0;
while i < input.len() {
let unit = input[i];
let normalized = if unit == b'\r'.into() {
if input.get(i + 1) == Some(&u16::from(b'\n')) {
i += 1;
}
u16::from(b'\n')
} else {
unit
};
if matches!(state, State::Normal) && at_line_start && normalized == u16::from(b'#') {
let start = i;
let mut end = i;
while end < input.len()
&& input[end] != u16::from(b'\n')
&& input[end] != u16::from(b'\r')
{
end += 1;
}
let directive = units_to_string(&input[start..end]);
if let Some((line, name)) = parse_line_directive(&directive) {
if let Some(name) = name {
directive_name = Some(name);
}
let removed = end - start;
lm.comment(i32_len(output.len())?, i32_len(removed)?)?;
lm.set_logical_line(line)?;
i = end;
continue;
}
}
match state {
State::Normal => {
if normalized == u16::from(b'/') {
state = State::Slash;
} else {
output.push(normalized);
if normalized == u16::from(b'\n') {
lm.newline(i32_len(output.len())?)?;
at_line_start = true;
} else {
at_line_start = false;
}
}
}
State::Slash => {
if normalized == u16::from(b'/') {
state = State::LineComment { removed: 2 };
} else if normalized == u16::from(b'*') {
state = State::BlockComment { removed: 2 };
} else {
output.push(u16::from(b'/'));
output.push(normalized);
at_line_start = false;
state = State::Normal;
}
}
State::LineComment { removed } => {
if normalized == u16::from(b'\n') {
lm.comment(i32_len(output.len())?, i32_len(removed)?)?;
output.push(normalized);
lm.newline(i32_len(output.len())?)?;
at_line_start = true;
state = State::Normal;
} else {
state = State::LineComment {
removed: removed + 1,
};
}
}
State::BlockComment { removed } => {
if normalized == u16::from(b'*') {
state = State::BlockStar {
removed: removed + 1,
};
} else if normalized == u16::from(b'\n') {
lm.comment(i32_len(output.len())?, i32_len(removed)?)?;
output.push(normalized);
lm.newline(i32_len(output.len())?)?;
at_line_start = true;
state = State::BlockComment { removed: 0 };
} else {
state = State::BlockComment {
removed: removed + 1,
};
}
}
State::BlockStar { removed } => {
if normalized == u16::from(b'/') {
lm.comment(i32_len(output.len())?, i32_len(removed + 1)?)?;
state = State::Normal;
} else if normalized == u16::from(b'*') {
state = State::BlockStar {
removed: removed + 1,
};
} else if normalized == u16::from(b'\n') {
lm.comment(i32_len(output.len())?, i32_len(removed)?)?;
output.push(normalized);
lm.newline(i32_len(output.len())?)?;
at_line_start = true;
state = State::BlockComment { removed: 0 };
} else {
state = State::BlockComment {
removed: removed + 1,
};
}
}
}
i += 1;
}
match state {
State::Normal => {}
State::Slash => output.push(u16::from(b'/')),
State::LineComment { removed } => {
lm.comment(i32_len(output.len())?, i32_len(removed)?)?;
}
State::BlockComment { .. } | State::BlockStar { .. } => {
return Err(Error::Parse {
position: input.len(),
context: "unterminated block comment",
});
}
}
lm.end = i32_len(output.len())?;
Ok((output, lm, directive_name))
}
fn parse_line_directive(value: &str) -> Option<(i32, Option<String>)> {
let value = value.strip_prefix('#')?.trim_start();
let value = value.strip_prefix("line")?.trim_start();
let (number, rest) = value.split_once(char::is_whitespace).unwrap_or((value, ""));
let line = number.parse::<i32>().ok()?;
if line < 1 {
return None;
}
let rest = rest.trim();
let name = rest
.strip_prefix('"')
.and_then(|quoted| quoted.strip_suffix('"'))
.map(ToOwned::to_owned);
Some((line, name))
}
/// A character predicate on one UTF-16 code unit.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CharacterMatcher {
Any,
Category(DotNetUnicodeCategory),
Categories(Vec<DotNetUnicodeCategory>),
UnicodeClass(UnicodeClass),
Eof,
Exact(u16),
Range { first: u16, last: u16 },
Set(BTreeSet<u16>),
}
impl CharacterMatcher {
fn matches(&self, value: Option<u16>) -> bool {
match (self, value) {
(Self::Eof, None) | (Self::Any, Some(_)) => true,
(Self::Category(category), Some(unit)) => {
DotNetUnicodeCategory::of_unit(unit) == *category
}
(Self::Categories(categories), Some(unit)) => {
categories.contains(&DotNetUnicodeCategory::of_unit(unit))
}
(Self::UnicodeClass(class), Some(unit)) => class.matches(unit),
(Self::Exact(expected), Some(unit)) => unit == *expected,
(Self::Range { first, last }, Some(unit)) => (*first..=*last).contains(&unit),
(Self::Set(values), Some(unit)) => values.contains(&unit),
_ => false,
}
}
fn stable_text(&self) -> String {
match self {
Self::Any => "any".to_owned(),
Self::Category(category) => format!("category:{}", category.name()),
Self::Categories(categories) => format!(
"categories:{}",
categories
.iter()
.map(|category| category.name())
.collect::<Vec<_>>()
.join(",")
),
Self::UnicodeClass(class) => format!("class:{class:?}"),
Self::Eof => "eof".to_owned(),
Self::Exact(unit) => format!("exact:{unit:04X}"),
Self::Range { first, last } => format!("range:{first:04X}-{last:04X}"),
Self::Set(values) => {
let values = values
.iter()
.map(|value| format!("{value:04X}"))
.collect::<Vec<_>>();
format!("set:{}", values.join(","))
}
}
}
}
/// Named character predicates accepted by the original lexer specification.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum UnicodeClass {
Symbol,
Punctuation,
Separator,
WhiteSpace,
Number,
Digit,
Letter,
Lower,
Upper,
}
impl UnicodeClass {
fn matches(self, unit: u16) -> bool {
let category = DotNetUnicodeCategory::of_unit(unit);
match self {
Self::Symbol => matches!(
category,
DotNetUnicodeCategory::MathSymbol
| DotNetUnicodeCategory::CurrencySymbol
| DotNetUnicodeCategory::ModifierSymbol
| DotNetUnicodeCategory::OtherSymbol
),
Self::Punctuation => matches!(
category,
DotNetUnicodeCategory::ConnectorPunctuation
| DotNetUnicodeCategory::DashPunctuation
| DotNetUnicodeCategory::OpenPunctuation
| DotNetUnicodeCategory::ClosePunctuation
| DotNetUnicodeCategory::InitialQuotePunctuation
| DotNetUnicodeCategory::FinalQuotePunctuation
| DotNetUnicodeCategory::OtherPunctuation
),
Self::Separator => matches!(
category,
DotNetUnicodeCategory::SpaceSeparator
| DotNetUnicodeCategory::LineSeparator
| DotNetUnicodeCategory::ParagraphSeparator
),
Self::WhiteSpace => {
matches!(unit, 0x0009..=0x000D | 0x0085) || Self::Separator.matches(unit)
}
Self::Number => matches!(
category,
DotNetUnicodeCategory::DecimalDigitNumber
| DotNetUnicodeCategory::LetterNumber
| DotNetUnicodeCategory::OtherNumber
),
Self::Digit => category == DotNetUnicodeCategory::DecimalDigitNumber,
Self::Letter => matches!(
category,
DotNetUnicodeCategory::UppercaseLetter
| DotNetUnicodeCategory::LowercaseLetter
| DotNetUnicodeCategory::TitlecaseLetter
| DotNetUnicodeCategory::ModifierLetter
| DotNetUnicodeCategory::OtherLetter
),
Self::Lower => category == DotNetUnicodeCategory::LowercaseLetter,
Self::Upper => category == DotNetUnicodeCategory::UppercaseLetter,
}
}
}
/// Lexer action attached to an accepting DFA state.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum LexerAction {
Emit,
EmitAndBegin(String),
Skip,
SkipAndBegin(String),
}
/// Token metadata stored in a deterministic lexer table.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TokenDefinition {
pub name: String,
pub number: i32,
}
impl TokenDefinition {
pub fn new(name: impl Into<String>, number: i32) -> Result<Self, Error> {
let name = name.into();
if name.is_empty() || number < 0 {
return Err(Error::Argument);
}
Ok(Self { name, number })
}
}
/// Accepting metadata for a DFA state.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DfaAccept {
pub token: TokenDefinition,
pub action: LexerAction,
pub action_number: i32,
pub reserved_words: Option<String>,
}
/// One deterministic automaton state.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct DfaState {
pub transitions: Vec<(CharacterMatcher, usize)>,
pub accept: Option<DfaAccept>,
}
impl DfaState {
#[must_use]
pub fn transition(mut self, matcher: CharacterMatcher, target: usize) -> Self {
self.transitions.push((matcher, target));
self
}
#[must_use]
pub fn accepting(mut self, accept: DfaAccept) -> Self {
self.accept = Some(accept);
self
}
}
/// Compatibility linked action.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DfaAction {
pub a_act: i32,
pub a_next: Option<Box<DfaAction>>,
}
impl DfaAction {
pub fn new(action: i32, next: DfaAction) -> Result<Self, Error> {
Ok(Self {
a_act: action,
a_next: Some(Box::new(next)),
})
}
pub fn serialise(o: Object, _s: Serialiser) -> Result<Object, Error> {
let _ = o;
Err(Error::InvalidOperation)
}
}
/// Runtime deterministic finite automaton.
#[derive(Clone, Debug)]
struct RegexDfa {
source: String,
compiled: regex::Regex,
accept: DfaAccept,
}
impl PartialEq for RegexDfa {
fn eq(&self, other: &Self) -> bool {
self.source == other.source && self.accept == other.accept
}
}
impl Eq for RegexDfa {}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Dfa {
pub m_actions: Vec<DfaAction>,
pub m_map: Hashtable,
pub m_reswds: i32,
pub m_tok_class: String,
states: Vec<DfaState>,
start: usize,
regex: Option<RegexDfa>,
}
impl Default for Dfa {
fn default() -> Self {
Self {
m_actions: Vec::new(),
m_map: Hashtable::default(),
m_reswds: -1,
m_tok_class: String::new(),
states: vec![DfaState::default()],
start: 0,
regex: None,
}
}
}
impl Dfa {
pub fn from_states(states: Vec<DfaState>, start: usize) -> Result<Self, Error> {
if states.is_empty() || start >= states.len() {
return Err(Error::Argument);
}
if states
.iter()
.flat_map(|state| &state.transitions)
.any(|(_, target)| *target >= states.len())
{
return Err(Error::Argument);
}
Ok(Self {
states,
start,
..Self::default()
})
}
pub fn new_with_nfa(nfa: Nfa) -> Result<Self, Error> {
let name = if nfa.m_end.m_s_terminal.is_empty() {
"ANY".to_owned()
} else {
nfa.m_end.m_s_terminal.clone()
};
let accept = DfaAccept {
token: TokenDefinition::new(name, 7)?,
action: LexerAction::Emit,
action_number: 0,
reserved_words: None,
};
if let Some(source) = nfa.regex_source()? {
let compiled =
regex::Regex::new(&format!("^(?:{source})")).map_err(|_| Error::Parse {
position: 0,
context: "invalid NFA regular expression",
})?;
return Ok(Self {
regex: Some(RegexDfa {
source,
compiled,
accept,
}),
..Self::default()
});
}
Self::from_states(nfa.deterministic_states(accept)?, 0)
}
pub fn new_with_tokens_gen(_tokens: TokensGen) -> Result<Self, Error> {
// The C# constructor creates the initial, unpopulated determinisation
// node. NFA-based construction is exposed separately by
// `new_with_nfa`; returning an empty native state here preserves that
// useful distinction without manufacturing an NFA transition.
Ok(Self::default())
}
fn longest_match(&self, units: &[u16], offset: usize) -> Option<(usize, DfaAccept)> {
if let Some(regex) = &self.regex {
let text = String::from_utf16(units.get(offset..)?).ok()?;
let found = regex.compiled.find(&text)?;
if found.start() != 0 || found.end() == 0 {
return None;
}
let length = text[..found.end()].encode_utf16().count();
return Some((length, regex.accept.clone()));
}
let mut state_index = self.start;
let mut cursor = offset;
let mut accepted = self.states[state_index]
.accept
.clone()
.map(|accept| (0, accept));
loop {
let value = units.get(cursor).copied();
let next = self.states[state_index]
.transitions
.iter()
.find(|(matcher, _)| matcher.matches(value))
.map(|(_, target)| *target);
let Some(next) = next else {
break;
};
// EOF transitions do not consume a code unit.
if value.is_some() {
cursor += 1;
}
state_index = next;
if let Some(accept) = self.states[state_index].accept.clone() {
accepted = Some((cursor - offset, accept));
}
if value.is_none() {
break;
}
}
accepted
}
pub fn match_(&self, value: String, offset: i32, action: &mut i32) -> Result<i32, Error> {
let units = value.encode_utf16().collect::<Vec<_>>();
let offset = index(offset)?;
if offset > units.len() {
return Err(Error::IndexOutOfRange);
}
let Some((length, accept)) = self.longest_match(&units, offset) else {
return Ok(-1);
};
*action = accept.action_number;
i32_len(length)
}
pub fn print(&self) -> Result<(), Error> {
println!("{}", self.stable_description());
Ok(())
}
#[must_use]
pub fn stable_description(&self) -> String {
if let Some(regex) = &self.regex {
return format!(
"regex {:?} accept {} {} {}",
regex.source,
regex.accept.token.number,
regex.accept.token.name,
regex.accept.action_number
);
}
let mut lines = Vec::new();
for (state_index, state) in self.states.iter().enumerate() {
if let Some(accept) = &state.accept {
lines.push(format!(
"state {state_index} accept {} {} {}",
accept.token.number, accept.token.name, accept.action_number
));
}
for (matcher, target) in &state.transitions {
lines.push(format!(
"state {state_index} {} -> {target}",
matcher.stable_text()
));
}
}
lines.join("\n")
}
pub fn serialise(o: Object, _s: Serialiser) -> Result<Object, Error> {
let _ = o;
Err(Error::InvalidOperation)
}
pub fn set_tokens(_tokens: YyLexer, _starts: Hashtable) -> Result<(), Error> {
Err(Error::InvalidOperation)
}
}
/// Reserved-word remapping for a token class.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ResWds {
pub m_wds: BTreeMap<String, TokenDefinition>,
pub m_upper: bool,
}
impl ResWds {
pub fn new_with_constructor() -> Result<Self, Error> {
Ok(Self::default())
}
pub fn new_with_tokens_gen_string(
tokens: TokensGen,
specification: String,
) -> Result<Self, Error> {
let mut specification = specification.trim();
let uppercase = specification.starts_with('U');
if uppercase {
specification = specification[1..].trim_start();
}
let Some(body) = specification
.strip_prefix('{')
.and_then(|value| value.strip_suffix('}'))
else {
return Err(Error::Parse {
position: 0,
context: "bad ResWds element",
});
};
let mut pairs = Vec::new();
for item in body.split(',') {
let mut fields = item.split_whitespace();
let Some(word) = fields.next() else {
continue;
};
let name = fields.next().unwrap_or(word);
if fields.next().is_some() {
return Err(Error::Parse {
position: specification.find(item).unwrap_or(0),
context: "reserved word has too many fields",
});
}
let key = Object::String(name.to_owned());
let Some(value) = tokens.m_tokens.tokens.0.get(&key) else {
// Creating a missing token class mutates generator state and is
// deliberately owned by issue #83. Never invent its number.
return Err(Error::InvalidOperation);
};
let (token_name, token_number) = match value {
Object::Integer(number) => (name.to_owned(), *number),
Object::Map(metadata) => {
let token_name = match metadata.get("name") {
Some(Object::String(value)) => value.clone(),
None => name.to_owned(),
_ => return Err(Error::InvalidOperation),
};
let Some(Object::Integer(number)) = metadata.get("number") else {
return Err(Error::InvalidOperation);
};
(token_name, *number)
}
_ => return Err(Error::InvalidOperation),
};
pairs.push((
word.to_owned(),
TokenDefinition::new(token_name, token_number)?,
));
}
Self::from_pairs(pairs, uppercase)
}
pub fn from_pairs<I, K>(pairs: I, uppercase: bool) -> Result<Self, Error>
where
I: IntoIterator<Item = (K, TokenDefinition)>,
K: Into<String>,
{
let mut words = BTreeMap::new();
for (word, token) in pairs {
let word = word.into();
if word.is_empty() {
return Err(Error::Argument);
}
let key = if uppercase { word.to_uppercase() } else { word };
words.insert(key, token);
}
Ok(Self {
m_wds: words,
m_upper: uppercase,
})
}
fn remap(&self, text: &str) -> Option<TokenDefinition> {
let key = if self.m_upper {
text.to_uppercase()
} else {
text.to_owned()
};
self.m_wds.get(&key).cloned()
}
pub fn check(&self, _lexer: Lexer, token: &mut TOKEN) -> Result<(), Error> {
if let Some(definition) = self.remap(&token.text) {
token.name = definition.name;
token.number = definition.number;
}
Ok(())
}
pub fn serialise(o: Object, _s: Serialiser) -> Result<Object, Error> {
let _ = o;
Err(Error::InvalidOperation)
}
}
/// Lexer table/configuration corresponding to `YyLexer`.
#[derive(Clone, Debug)]
pub struct YyLexer {
pub cats: Hashtable,
pub erh: ErrorHandler,
pub m_encoding: TextEncoding,
pub m_gencat: UnicodeCategory,
pub reswds: Hashtable,
pub starts: Hashtable,
pub tokens: Hashtable,
pub toupper: bool,
pub types: Hashtable,
pub using_eof: bool,
encoding: InputEncoding,
categories: BTreeMap<DotNetUnicodeCategory, Charset>,
start_dfas: BTreeMap<String, Dfa>,
reserved: BTreeMap<String, ResWds>,
}
impl YyLexer {
pub fn new(error_handler: ErrorHandler) -> Result<Self, Error> {
let default_category = DotNetUnicodeCategory::OtherPunctuation;
let mut categories = BTreeMap::new();
categories.insert(default_category, Charset::for_category(default_category));
Ok(Self {
cats: Hashtable::default(),
erh: error_handler,
m_encoding: TextEncoding,
m_gencat: UnicodeCategory(default_category as i32),
reswds: Hashtable::default(),
starts: Hashtable::default(),
tokens: Hashtable::default(),
toupper: false,
types: Hashtable::default(),
using_eof: false,
encoding: InputEncoding::Utf8,
categories,
start_dfas: BTreeMap::new(),
reserved: BTreeMap::new(),
})
}
pub fn set_start_dfa(&mut self, state: impl Into<String>, dfa: Dfa) -> Result<(), Error> {
let state = state.into();
if state.is_empty() {
return Err(Error::Argument);
}
self.start_dfas.insert(state, dfa);
Ok(())
}
pub fn set_reserved_words(
&mut self,
name: impl Into<String>,
words: ResWds,
) -> Result<(), Error> {
let name = name.into();
if name.is_empty() {
return Err(Error::Argument);
}
self.reserved.insert(name, words);
Ok(())
}
pub fn set_input_encoding(&mut self, value: String) {
if let Ok(encoding) = InputEncoding::parse(&value) {
self.toupper = encoding == InputEncoding::AsciiUpper;
self.encoding = encoding;
} else if let Ok(error) = CSToolsException::new_with_int32_string(
43,
format!("Warning: Encoding {value} unknown: ignored"),
) {
let _ = self.erh.error(error);
}
}
#[must_use]
pub const fn input_encoding(&self) -> InputEncoding {
self.encoding
}
pub fn using_cat(&mut self, category: UnicodeCategory) -> Result<Charset, Error> {
let category = DotNetUnicodeCategory::from_compat(category)?;
Ok(self
.categories
.entry(category)
.or_insert_with(|| Charset::for_category(category))
.clone())
}
pub fn get_test(&mut self, name: &str) -> Result<CharacterMatcher, Error> {
if let Some(category) = (0..=29)
.filter_map(|value| DotNetUnicodeCategory::try_from(value).ok())
.find(|category| category.name() == name)
{
let _ = self.using_cat(UnicodeCategory(category as i32))?;
return Ok(CharacterMatcher::Category(category));
}
let (class, categories): (UnicodeClass, &[DotNetUnicodeCategory]) = match name {
"Symbol" => (
UnicodeClass::Symbol,
&[
DotNetUnicodeCategory::MathSymbol,
DotNetUnicodeCategory::CurrencySymbol,
DotNetUnicodeCategory::ModifierSymbol,
DotNetUnicodeCategory::OtherSymbol,
],
),
"Punctuation" => (
UnicodeClass::Punctuation,
&[
DotNetUnicodeCategory::ConnectorPunctuation,
DotNetUnicodeCategory::DashPunctuation,
DotNetUnicodeCategory::OpenPunctuation,
DotNetUnicodeCategory::ClosePunctuation,
DotNetUnicodeCategory::InitialQuotePunctuation,
DotNetUnicodeCategory::FinalQuotePunctuation,
DotNetUnicodeCategory::OtherPunctuation,
],
),
"Separator" => (
UnicodeClass::Separator,
&[
DotNetUnicodeCategory::SpaceSeparator,
DotNetUnicodeCategory::LineSeparator,
DotNetUnicodeCategory::ParagraphSeparator,
],
),
"Number" => (
UnicodeClass::Number,
&[
DotNetUnicodeCategory::DecimalDigitNumber,
DotNetUnicodeCategory::LetterNumber,
DotNetUnicodeCategory::OtherNumber,
],
),
"Letter" => (
UnicodeClass::Letter,
&[
DotNetUnicodeCategory::UppercaseLetter,
DotNetUnicodeCategory::LowercaseLetter,
DotNetUnicodeCategory::TitlecaseLetter,
DotNetUnicodeCategory::ModifierLetter,
DotNetUnicodeCategory::OtherLetter,
],
),
"Digit" => (
UnicodeClass::Digit,
&[DotNetUnicodeCategory::DecimalDigitNumber],
),
"Lower" => (
UnicodeClass::Lower,
&[DotNetUnicodeCategory::LowercaseLetter],
),
"Upper" => (
UnicodeClass::Upper,
&[DotNetUnicodeCategory::UppercaseLetter],
),
"EOF" => {
self.using_eof = true;
return Ok(CharacterMatcher::Eof);
}
"WhiteSpace" => (
UnicodeClass::WhiteSpace,
&[
DotNetUnicodeCategory::Control,
DotNetUnicodeCategory::SpaceSeparator,
DotNetUnicodeCategory::LineSeparator,
DotNetUnicodeCategory::ParagraphSeparator,
],
),
_ => {
if let Ok(error) =
CSToolsException::new_with_int32_string(24, format!("No such Charset {name}"))
{
self.erh.error(error)?;
}
return Err(Error::Argument);
}
};
for category in categories {
let _ = self.using_cat(UnicodeCategory(*category as i32))?;
}
Ok(CharacterMatcher::UnicodeClass(class))
}
#[must_use]
pub fn category_group(name: &str) -> Option<&'static [DotNetUnicodeCategory]> {
match name {
"WhiteSpace" => Some(&[
DotNetUnicodeCategory::Control,
DotNetUnicodeCategory::SpaceSeparator,
DotNetUnicodeCategory::LineSeparator,
DotNetUnicodeCategory::ParagraphSeparator,
]),
"Letter" => Some(&[
DotNetUnicodeCategory::UppercaseLetter,
DotNetUnicodeCategory::LowercaseLetter,
DotNetUnicodeCategory::TitlecaseLetter,
DotNetUnicodeCategory::ModifierLetter,
DotNetUnicodeCategory::OtherLetter,
]),
"Number" => Some(&[
DotNetUnicodeCategory::DecimalDigitNumber,
DotNetUnicodeCategory::LetterNumber,
DotNetUnicodeCategory::OtherNumber,
]),
_ => None,
}
}
pub fn get_dfa(&self) -> Result<(), Error> {
if self.start_dfas.is_empty() {
return Err(Error::InvalidOperation);
}
Ok(())
}
pub fn emit_dfa(&self, mut output: Box<dyn Write + Send>) -> Result<(), Error> {
writeln!(output, "LSLLEXER 1").map_err(|_| Error::InvalidOperation)?;
writeln!(output, "encoding {:?}", self.encoding).map_err(|_| Error::InvalidOperation)?;
writeln!(output, "toupper {}", self.toupper).map_err(|_| Error::InvalidOperation)?;
writeln!(output, "eof {}", self.using_eof).map_err(|_| Error::InvalidOperation)?;
for (name, dfa) in &self.start_dfas {
writeln!(output, "start {name}").map_err(|_| Error::InvalidOperation)?;
writeln!(output, "{}", dfa.stable_description())
.map_err(|_| Error::InvalidOperation)?;
}
for (name, words) in &self.reserved {
for (word, token) in &words.m_wds {
writeln!(
output,
"reserved {name} {word} {} {}",
token.number, token.name
)
.map_err(|_| Error::InvalidOperation)?;
}
}
Ok(())
}
pub fn get_enumerator(&self) -> Result<IEnumerator, Error> {
Ok(IEnumerator)
}
#[allow(clippy::ptr_arg)]
pub fn old_action(
&self,
lexer: Lexer,
yytext: &mut String,
action: i32,
reject: &mut bool,
) -> Result<TOKEN, Error> {
*reject = false;
let end = lexer.m_pch;
let length = i32_len(utf16_len(yytext))?;
let mut token = TOKEN::new_with_lexer_string(lexer, yytext.clone())?;
token.number = action;
token.pos = end.checked_sub(length).ok_or(Error::IndexOutOfRange)?;
token.end = end;
Ok(token)
}
fn dfa(&self, state: &str) -> Option<&Dfa> {
self.start_dfas.get(state)
}
fn reserved_word(&self, name: &str, text: &str) -> Option<TokenDefinition> {
self.reserved.get(name).and_then(|words| words.remap(text))
}
}
/// Base parser symbol with source location and semantic value.
#[allow(clippy::upper_case_acronyms)]
pub struct SYMBOL {
pub kids: ObjectList,
pub m_dollar: Object,
pub pos_with_field: i32,
pub yylx: Option<Box<Lexer>>,
pub yyps: Option<Box<Parser>>,
name: String,
number: i32,
text: String,
}
impl Clone for SYMBOL {
fn clone(&self) -> Self {
Self {
kids: self.kids.clone(),
m_dollar: self.m_dollar.clone(),
pos_with_field: self.pos_with_field,
yylx: self.yylx.clone(),
yyps: self.yyps.clone(),
name: self.name.clone(),
number: self.number,
text: self.text.clone(),
}
}
}
impl fmt::Debug for SYMBOL {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SYMBOL")
.field("kids", &self.kids)
.field("m_dollar", &self.m_dollar)
.field("pos", &self.pos_with_field)
.field("has_lexer", &self.yylx.is_some())
.field("has_parser", &self.yyps.is_some())
.field("name", &self.name)
.field("number", &self.number)
.field("text", &self.text)
.finish()
}
}
impl SYMBOL {
pub fn new_with_lexer(lexer: Lexer) -> Result<Self, Error> {
Ok(Self {
kids: ObjectList::new()?,
m_dollar: Object::Undefined,
pos_with_field: lexer.yypos(),
yylx: Some(Box::new(lexer)),
yyps: None,
name: "SYMBOL".to_owned(),
number: 0,
text: String::new(),
})
}
pub fn new_with_parser(parser: Parser) -> Result<Self, Error> {
Ok(Self {
kids: ObjectList::new()?,
m_dollar: Object::Undefined,
pos_with_field: 0,
yylx: None,
yyps: Some(Box::new(parser)),
name: "SYMBOL".to_owned(),
number: 0,
text: String::new(),
})
}
pub(crate) fn parser_symbol(
name: String,
number: i32,
text: String,
pos: i32,
value: Object,
kids: ObjectList,
) -> Self {
Self {
kids,
m_dollar: value,
pos_with_field: pos,
yylx: None,
yyps: None,
name,
number,
text,
}
}
fn location(&self) -> Result<SourceLineInfo, Error> {
self.yylx
.as_deref()
.ok_or(Error::InvalidOperation)?
.source_line_info(self.pos_with_field)
}
pub fn concrete_syntax_tree(&self) -> Result<(), Error> {
println!("{}", self.concrete_syntax_tree_text());
Ok(())
}
#[must_use]
pub fn concrete_syntax_tree_text(&self) -> String {
let mut output = self.yyname();
for child in self.kids.iter() {
output.push('\n');
let _ = std::fmt::write(&mut output, format_args!(" {child:?}"));
}
output
}
pub fn is_action(&self) -> Result<bool, Error> {
Ok(false)
}
pub fn is_c_symbol(&self) -> Result<bool, Error> {
Ok(false)
}
pub fn is_terminal(&self) -> Result<bool, Error> {
Ok(false)
}
pub fn matches(&self, value: String) -> Result<bool, Error> {
Ok(self.text == value)
}
pub fn pass_(
&self,
symbols: YyParser,
state: i32,
entry: &mut ParserEntry,
) -> Result<bool, Error> {
lookup_parser_entry(&symbols, self.yynum(), state, entry)
}
pub fn print(&self) -> Result<(), Error> {
println!("{}", self.to_string());
Ok(())
}
#[must_use]
pub fn to_string(&self) -> String {
if self.text.is_empty() {
self.yyname()
} else {
format!("{}<{}>", self.yyname(), self.text)
}
}
pub fn from(symbol: SYMBOL) -> i32 {
match symbol.m_dollar {
Object::Integer(value) => value,
_ => panic!("SYMBOL semantic value is not a System.Int32"),
}
}
#[must_use]
pub fn line(&self) -> i32 {
self.location().map_or(0, |value| value.line_number)
}
#[must_use]
pub fn pos_with_property(&self) -> String {
self.location()
.map_or_else(|_| String::new(), |value| value.to_string())
}
#[must_use]
pub fn position(&self) -> i32 {
self.location().map_or(0, |value| value.raw_char_position)
}
pub fn yyact(&self) -> Result<&YyParser, Error> {
self.yyps
.as_deref()
.map(|parser| &parser.m_symbols)
.ok_or(Error::InvalidOperation)
}
#[must_use]
pub fn yylval(&self) -> Object {
self.m_dollar.clone()
}
pub fn set_yylval(&mut self, value: Object) {
self.m_dollar = value;
}
pub(crate) fn raw_text(&self) -> String {
self.text.clone()
}
#[must_use]
pub fn yyname(&self) -> String {
self.name.clone()
}
#[must_use]
pub const fn yynum(&self) -> i32 {
self.number
}
}
impl fmt::Display for SYMBOL {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.yyname())
}
}
/// Terminal token emitted by [`Lexer`].
#[allow(clippy::upper_case_acronyms)]
#[derive(Clone, Debug)]
pub struct TOKEN {
text: String,
name: String,
number: i32,
pub pos: i32,
pub end: i32,
value: Object,
source: Option<Box<Lexer>>,
}
impl TOKEN {
pub(crate) fn generated_with_lexer(
lexer: Lexer,
name: &str,
number: i32,
) -> Result<Self, Error> {
let mut token = Self::new_with_lexer(lexer)?;
name.clone_into(&mut token.name);
token.number = number;
Ok(token)
}
pub(crate) fn generated_with_parser(
parser: Parser,
name: &str,
number: i32,
text: String,
) -> Result<Self, Error> {
let mut token = Self::new_with_parser(parser)?;
name.clone_into(&mut token.name);
token.number = number;
token.text = text;
Ok(token)
}
pub fn new_with_lexer(lexer: Lexer) -> Result<Self, Error> {
let text = lexer.yytext.clone();
Self::new_with_lexer_string(lexer, text)
}
pub fn new_with_lexer_string(lexer: Lexer, text: String) -> Result<Self, Error> {
let pos = lexer.yypos();
let end = pos
.checked_add(i32_len(utf16_len(&text))?)
.ok_or(Error::IndexOutOfRange)?;
Ok(Self {
text,
name: "TOKEN".to_owned(),
number: 1,
pos,
end,
value: Object::Undefined,
source: Some(Box::new(lexer)),
})
}
pub fn new_with_parser(_parser: Parser) -> Result<Self, Error> {
Ok(Self {
text: String::new(),
name: "TOKEN".to_owned(),
number: 1,
pos: 0,
end: 0,
value: Object::Undefined,
source: None,
})
}
fn emitted(
lexer: &Lexer,
definition: TokenDefinition,
text: String,
pos: usize,
end: usize,
) -> Result<Self, Error> {
Ok(Self {
text,
name: definition.name,
number: definition.number,
pos: i32_len(pos)?,
end: i32_len(end)?,
value: Object::Undefined,
source: Some(Box::new(lexer.clone())),
})
}
fn eof(lexer: &Lexer) -> Result<Self, Error> {
let pos = lexer.units.len();
Self::emitted(
lexer,
TokenDefinition {
name: "EOF".to_owned(),
number: 2,
},
"EOF".to_owned(),
pos,
pos,
)
}
fn location(&self) -> Result<SourceLineInfo, Error> {
self.source
.as_deref()
.ok_or(Error::InvalidOperation)?
.source_line_info(self.pos)
}
pub fn is_terminal(&self) -> Result<bool, Error> {
Ok(true)
}
pub fn matches(&self, value: String) -> Result<bool, Error> {
Ok(self.text == value)
}
pub fn pass_(
&self,
symbols: YyParser,
state: i32,
entry: &mut ParserEntry,
) -> Result<bool, Error> {
let number = match symbols.literals.0.get(&Object::String(self.text.clone())) {
Some(Object::Integer(number)) => *number,
_ => self.number,
};
lookup_parser_entry(&symbols, number, state, entry)
}
pub fn print(&self) -> Result<(), Error> {
println!("{}", self.to_string());
Ok(())
}
#[must_use]
pub fn to_string(&self) -> String {
format!("{}<{}>", self.name, self.text)
}
#[must_use]
pub fn yyname(&self) -> String {
self.name.clone()
}
#[must_use]
pub const fn yynum(&self) -> i32 {
self.number
}
#[must_use]
pub fn yytext(&self) -> String {
self.text.clone()
}
pub fn set_yytext(&mut self, value: String) {
self.text = value;
}
#[must_use]
pub fn yylval(&self) -> Object {
self.value.clone()
}
pub fn set_yylval(&mut self, value: Object) {
self.value = value;
}
}
impl fmt::Display for TOKEN {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.to_string())
}
}
fn lookup_parser_entry(
symbols: &YyParser,
number: i32,
state: i32,
entry: &mut ParserEntry,
) -> Result<bool, Error> {
if let Some(native) = symbols.lookup_entry(number, state)? {
*entry = native;
return Ok(true);
}
let Some(info) = symbols.symbol_info.0.get(&Object::Integer(number)) else {
return Err(Error::Parse {
position: 0,
context: "parser does not recognize token or symbol",
});
};
let Object::Map(states) = info else {
return Err(Error::Parse {
position: 0,
context: "parser symbol information is malformed",
});
};
let Some(value) = states.get(&state.to_string()) else {
return Ok(false);
};
entry.m_priority = match value {
Object::Integer(priority) => *priority,
Object::Map(metadata) => match metadata.get("priority") {
Some(Object::Integer(priority)) => *priority,
_ => 0,
},
_ => {
return Err(Error::Parse {
position: 0,
context: "parser entry is malformed",
});
}
};
Ok(true)
}
/// End-of-input terminal.
#[allow(clippy::upper_case_acronyms)]
#[derive(Clone, Debug)]
pub struct EOF(pub TOKEN);
impl EOF {
pub fn new_with_lexer(lexer: Lexer) -> Result<Self, Error> {
Ok(Self(TOKEN::eof(&lexer)?))
}
pub fn new_with_symbols_gen(_symbols: SymbolsGen) -> Result<Self, Error> {
Ok(Self(TOKEN {
text: "EOF".to_owned(),
name: "EOF".to_owned(),
number: 2,
pos: 0,
end: 0,
value: Object::Undefined,
source: None,
}))
}
pub fn serialise(o: Object, _s: Serialiser) -> Result<Object, Error> {
let _ = o;
Err(Error::InvalidOperation)
}
#[must_use]
pub fn yyname(&self) -> String {
"EOF".to_owned()
}
#[must_use]
pub const fn yynum(&self) -> i32 {
2
}
}
/// A proxy token used by generated grammars for nullable productions.
#[derive(Clone, Debug)]
pub struct Null {
proxy: String,
}
impl Null {
pub fn new_with_lexer_string(_lexer: Lexer, proxy: String) -> Result<Self, Error> {
if proxy.is_empty() {
return Err(Error::Argument);
}
Ok(Self { proxy })
}
pub fn new_with_parser_string(_parser: Parser, proxy: String) -> Result<Self, Error> {
if proxy.is_empty() {
return Err(Error::Argument);
}
Ok(Self { proxy })
}
#[must_use]
pub fn yyname(&self) -> String {
self.proxy.clone()
}
}
/// Rust replacement for the C# cons-list, backed by contiguous storage.
#[derive(Clone, Debug, Default)]
pub struct ObjectList {
values: VecDeque<Object>,
count_override: Option<i32>,
}
impl ObjectList {
pub fn new() -> Result<Self, Error> {
Ok(Self::default())
}
pub fn add(&mut self, value: Object) -> Result<(), Error> {
self.values.push_back(value);
Ok(())
}
pub fn push(&mut self, value: Object) -> Result<(), Error> {
self.values.push_front(value);
Ok(())
}
pub fn pop(&mut self) -> Result<Object, Error> {
self.values.pop_front().ok_or(Error::InvalidOperation)
}
pub fn top(&self) -> Object {
self.values.front().cloned().unwrap_or(Object::Undefined)
}
pub fn get_enumerator(&self) -> Result<IEnumerator, Error> {
Ok(IEnumerator)
}
#[must_use]
pub fn count(&self) -> i32 {
self.count_override
.unwrap_or_else(|| i32::try_from(self.values.len()).unwrap_or(i32::MAX))
}
pub fn set_count(&mut self, value: i32) {
let Ok(value) = usize::try_from(value) else {
return;
};
self.values.resize(value, Object::Undefined);
self.count_override = None;
}
#[must_use]
pub fn item(&self, item_index: i32) -> Object {
index(item_index)
.ok()
.and_then(|index| self.values.get(index))
.cloned()
.unwrap_or(Object::Undefined)
}
pub fn set_item(&mut self, index_value: i32, value: Object) {
if let Ok(index) = index(index_value)
&& let Some(slot) = self.values.get_mut(index)
{
*slot = value;
}
}
pub fn iter(&self) -> impl Iterator<Item = &Object> {
self.values.iter()
}
}
/// Runtime lexer over one immutable filtered source buffer.
#[derive(Clone, Debug)]
pub struct Lexer {
pub m_buf: String,
pub m_debug: bool,
pub m_pch: i32,
pub m_state: String,
pub yytext: String,
pub m_line_manager: LineManager,
tokens: YyLexer,
units: Vec<u16>,
cursor: usize,
start_match: usize,
eof_emitted: bool,
}
impl Lexer {
pub fn new(tokens: YyLexer) -> Result<Self, Error> {
Ok(Self {
m_buf: String::new(),
m_debug: false,
m_pch: 0,
m_state: "YYINITIAL".to_owned(),
yytext: String::new(),
m_line_manager: LineManager::default(),
tokens,
units: Vec::new(),
cursor: 0,
start_match: 0,
eof_emitted: false,
})
}
pub fn start_with_string(&mut self, source: String) -> Result<(), Error> {
self.start_with_cs_reader(CsReader::new_with_string(format!("{source}\n"))?)
}
pub fn start_with_stream_reader(&mut self, reader: StreamReader) -> Result<(), Error> {
let source = self.tokens.encoding.decode(&reader.0)?;
self.start_with_cs_reader(CsReader::new_with_string(source)?)
}
pub fn start_with_cs_reader(&mut self, reader: CsReader) -> Result<(), Error> {
let (_, units, manager) = reader.into_parts();
if units.len() > MAX_SOURCE_UNITS {
return Err(Error::Argument);
}
self.m_buf = units_to_string(&units);
self.units = units;
self.m_line_manager = manager;
self.reset()?;
Ok(())
}
pub fn reset(&mut self) -> Result<(), Error> {
self.cursor = 0;
self.start_match = 0;
self.m_pch = 0;
self.yytext.clear();
self.eof_emitted = false;
Ok(())
}
pub fn yy_begin(&mut self, state: String) -> Result<(), Error> {
if self.tokens.dfa(&state).is_none() {
self.record_diagnostic(
2,
DiagnosticCategory::InvalidState,
"unknown lexer start condition",
self.cursor,
)?;
return Err(Error::Parse {
position: self.cursor,
context: "unknown lexer start condition",
});
}
self.m_state = state;
Ok(())
}
pub fn next_token(&mut self) -> Result<Option<TOKEN>, Error> {
loop {
if self.cursor >= self.units.len() {
if self.tokens.using_eof && !self.eof_emitted {
self.eof_emitted = true;
"EOF".clone_into(&mut self.yytext);
return Ok(Some(TOKEN::eof(self)?));
}
return Ok(None);
}
let matched = self
.tokens
.dfa(&self.m_state)
.map(|dfa| dfa.longest_match(&self.units, self.cursor));
let Some(matched) = matched else {
self.record_diagnostic(
2,
DiagnosticCategory::InvalidState,
"lexer start condition has no DFA",
self.cursor,
)?;
return Err(Error::Parse {
position: self.cursor,
context: "lexer start condition has no DFA",
});
};
let Some((length, accept)) = matched else {
let position = self.cursor;
self.cursor += 1;
self.m_pch = i32_len(self.cursor)?;
self.record_diagnostic(
1,
DiagnosticCategory::InvalidCharacter,
"input does not match any lexer rule",
position,
)?;
return Err(Error::Parse {
position,
context: "input does not match any lexer rule",
});
};
if length == 0 {
self.record_diagnostic(
2,
DiagnosticCategory::InvalidState,
"non-EOF lexer rule accepted an empty token",
self.cursor,
)?;
return Err(Error::Parse {
position: self.cursor,
context: "non-EOF lexer rule accepted an empty token",
});
}
if length > MAX_TOKEN_UNITS {
self.record_diagnostic(
3,
DiagnosticCategory::TokenTooLong,
"token exceeds the configured size bound",
self.cursor,
)?;
return Err(Error::Parse {
position: self.cursor,
context: "token exceeds the configured size bound",
});
}
self.start_match = self.cursor;
let end = self.cursor + length;
let mut text = units_to_string(&self.units[self.cursor..end]);
if self.tokens.toupper {
text = text.to_uppercase();
}
self.cursor = end;
self.m_pch = i32_len(end)?;
self.yytext.clone_from(&text);
let mut definition = accept.token;
if let Some(table) = accept.reserved_words.as_deref()
&& let Some(reserved) = self.tokens.reserved_word(table, &text)
{
definition = reserved;
}
let should_emit = match accept.action {
LexerAction::Emit => true,
LexerAction::Skip => false,
LexerAction::EmitAndBegin(state) => {
self.yy_begin(state)?;
true
}
LexerAction::SkipAndBegin(state) => {
self.yy_begin(state)?;
false
}
};
if should_emit {
return Ok(Some(TOKEN::emitted(
self,
definition,
text,
self.start_match,
end,
)?));
}
}
}
pub fn next(&mut self) -> Result<TOKEN, Error> {
self.next_token()?.ok_or(Error::InvalidOperation)
}
pub fn get_enumerator(&self) -> Result<LexerEnumerator, Error> {
LexerEnumerator::new(self.clone())
}
#[must_use]
pub fn iter(&self) -> LexerIterator {
LexerIterator {
lexer: self.clone(),
finished: false,
}
}
pub fn peek_char(&self) -> Result<Utf16CodeUnit, Error> {
Ok(Utf16CodeUnit(
self.units
.get(self.cursor)
.copied()
.unwrap_or(if self.tokens.using_eof { u16::MAX } else { 0 }),
))
}
pub fn get_char(&mut self) -> Result<i32, Error> {
let value = self.peek_char()?.0;
if self.cursor < self.units.len() {
self.cursor += 1;
self.m_pch = i32_len(self.cursor)?;
}
Ok(i32::from(value))
}
pub fn advance(&mut self) -> Result<(), Error> {
let _ = self.get_char()?;
Ok(())
}
pub fn un_get_char(&mut self) -> Result<(), Error> {
if self.cursor == 0 {
return Err(Error::InvalidOperation);
}
self.cursor -= 1;
self.m_pch = i32_len(self.cursor)?;
Ok(())
}
pub fn source_line_info(&self, pos: i32) -> Result<SourceLineInfo, Error> {
SourceLineInfo::from_manager(&self.m_line_manager, pos, self.m_buf.clone())
}
pub fn source_line(&self, info: SourceLineInfo) -> Result<String, Error> {
Ok(info.source_line())
}
pub fn saypos(&self, pos: i32) -> Result<String, Error> {
Ok(self.source_line_info(pos)?.to_string())
}
fn record_diagnostic(
&mut self,
code: i32,
category: DiagnosticCategory,
message: &str,
position: usize,
) -> Result<(), Error> {
let location = self.source_line_info(i32_len(position)?)?;
let input = self
.units
.get(position)
.map(|unit| units_to_string(std::slice::from_ref(unit)))
.unwrap_or_default();
self.tokens.erh.push(Diagnostic {
code,
category,
severity: DiagnosticSeverity::Error,
message: message.to_owned(),
location,
input,
})
}
#[must_use]
pub fn diagnostics(&self) -> &[Diagnostic] {
self.tokens.erh.diagnostics()
}
#[must_use]
pub fn m_start(&self) -> Dfa {
self.tokens.dfa(&self.m_state).cloned().unwrap_or_default()
}
#[must_use]
pub fn tokens(&self) -> YyLexer {
self.tokens.clone()
}
pub fn set_tokens(&mut self, value: YyLexer) {
self.tokens = value;
}
#[must_use]
pub fn yypos(&self) -> i32 {
self.m_pch
}
}
/// Idiomatic Rust iterator over lexer results.
pub struct LexerIterator {
lexer: Lexer,
finished: bool,
}
impl Iterator for LexerIterator {
type Item = Result<TOKEN, Error>;
fn next(&mut self) -> Option<Self::Item> {
if self.finished {
return None;
}
match self.lexer.next_token() {
Ok(Some(token)) => Some(Ok(token)),
Ok(None) => {
self.finished = true;
None
}
Err(error) => {
self.finished = true;
Some(Err(error))
}
}
}
}
impl IntoIterator for &Lexer {
type Item = Result<TOKEN, Error>;
type IntoIter = LexerIterator;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
/// Compatibility enumerator backed by the same bounded lexer iterator.
#[derive(Clone, Debug)]
pub struct LexerEnumerator {
lexer: Lexer,
initial: Lexer,
current: Option<TOKEN>,
}
impl LexerEnumerator {
pub fn new(lexer: Lexer) -> Result<Self, Error> {
Ok(Self {
initial: lexer.clone(),
lexer,
current: None,
})
}
pub fn move_next(&mut self) -> Result<bool, Error> {
self.current = self.lexer.next_token()?;
Ok(self.current.is_some())
}
pub fn reset(&mut self) -> Result<(), Error> {
self.lexer = self.initial.clone();
self.current = None;
Ok(())
}
#[must_use]
pub fn current(&self) -> TOKEN {
self.current
.clone()
.expect("Current is unavailable before MoveNext")
}
pub fn set_current(&mut self, value: TOKEN) {
self.current = Some(value);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn definition(name: &str, number: i32) -> TokenDefinition {
TokenDefinition::new(name, number).expect("valid token definition")
}
fn accept(name: &str, number: i32, action: LexerAction) -> DfaAccept {
DfaAccept {
token: definition(name, number),
action,
action_number: 0,
reserved_words: None,
}
}
fn sample_lexer() -> Lexer {
let mut table = YyLexer::new(ErrorHandler::default()).expect("table");
table.using_eof = true;
let states = vec![
DfaState::default()
.transition(
CharacterMatcher::Range {
first: b'a'.into(),
last: b'z'.into(),
},
1,
)
.transition(CharacterMatcher::Exact(b' '.into()), 2)
.transition(CharacterMatcher::Exact(b'\n'.into()), 2),
DfaState::default()
.transition(
CharacterMatcher::Range {
first: b'a'.into(),
last: b'z'.into(),
},
1,
)
.accepting(accept("ID", 3, LexerAction::Emit)),
DfaState::default()
.transition(CharacterMatcher::Exact(b' '.into()), 2)
.transition(CharacterMatcher::Exact(b'\n'.into()), 2)
.accepting(accept("WS", 4, LexerAction::Skip)),
];
table
.set_start_dfa("YYINITIAL", Dfa::from_states(states, 0).expect("dfa"))
.expect("start");
let mut lexer = Lexer::new(table).expect("lexer");
lexer
.start_with_string("alpha beta".to_owned())
.expect("source");
lexer
}
#[test]
fn lexer_uses_maximum_munch_skips_and_emits_eof() {
let mut lexer = sample_lexer();
let first = lexer.next().expect("first");
let second = lexer.next().expect("second");
let eof = lexer.next().expect("eof");
assert_eq!(
(first.yyname(), first.yytext(), first.pos, first.end),
("ID".to_owned(), "alpha".to_owned(), 0, 5)
);
assert_eq!(
(second.yyname(), second.yytext(), second.pos, second.end),
("ID".to_owned(), "beta".to_owned(), 6, 10)
);
assert_eq!(
(eof.yyname(), eof.yynum(), eof.pos),
("EOF".to_owned(), 2, 11)
);
assert!(lexer.next_token().expect("done").is_none());
}
#[test]
fn comments_are_removed_but_raw_columns_are_preserved() {
let reader =
CsReader::new_with_string("ab/*xyz*/cd\n// comment\nef".to_owned()).expect("reader");
assert_eq!(units_to_string(&reader.units), "abcd\n\nef");
let first = SourceLineInfo::from_manager(&reader.lm, 2, units_to_string(&reader.units))
.expect("location");
let after = SourceLineInfo::from_manager(&reader.lm, 3, units_to_string(&reader.units))
.expect("location");
assert_eq!(first.raw_char_position, 2);
assert_eq!(after.raw_char_position, 10);
let third_line =
SourceLineInfo::from_manager(&reader.lm, 6, units_to_string(&reader.units))
.expect("location");
assert_eq!(third_line.line_number, 3);
}
#[test]
fn utf16_positions_do_not_turn_into_utf8_byte_offsets() {
let reader = CsReader::new_with_string("a😀b\n".to_owned()).expect("reader");
assert_eq!(reader.units.len(), 5);
let info = SourceLineInfo::from_manager(&reader.lm, 4, units_to_string(&reader.units))
.expect("location");
assert_eq!(info.char_position, 5);
}
#[test]
fn line_directives_change_logical_line_and_file() {
let reader = CsReader::new_with_string("#line 42 \"generated.cs\"\nvalue\n".to_owned())
.expect("reader");
assert_eq!(reader.fname, "generated.cs");
let info = SourceLineInfo::from_manager(&reader.lm, 1, units_to_string(&reader.units))
.expect("location");
assert_eq!(info.line_number, 42);
}
#[test]
fn unterminated_block_comment_has_stable_category_and_position() {
assert_eq!(
CsReader::new_with_string("a /* nope".to_owned()).expect_err("invalid"),
Error::Parse {
position: 9,
context: "unterminated block comment"
}
);
}
#[test]
fn unicode_categories_match_dotnet_numbering() {
assert!(
CatTest::new(UnicodeCategory(0))
.expect("category")
.test(Utf16CodeUnit('A' as u16))
.expect("test")
);
assert!(
CatTest::new(UnicodeCategory(25))
.expect("category")
.test(Utf16CodeUnit('+' as u16))
.expect("test")
);
assert!(
CatTest::new(UnicodeCategory(16))
.expect("category")
.test(Utf16CodeUnit(0xD800))
.expect("test")
);
}
#[test]
fn reserved_words_remap_only_the_configured_token() {
let mut lexer = sample_lexer();
lexer
.tokens
.set_reserved_words(
"keywords",
ResWds::from_pairs([("alpha", definition("ALPHA", 9))], false).expect("words"),
)
.expect("table");
if let Some(dfa) = lexer.tokens.start_dfas.get_mut("YYINITIAL") {
dfa.states[1]
.accept
.as_mut()
.expect("accept")
.reserved_words = Some("keywords".to_owned());
}
let first = lexer.next().expect("token");
assert_eq!((first.yyname(), first.yynum()), ("ALPHA".to_owned(), 9));
}
#[test]
fn encoded_input_is_validated_without_platform_code_pages() {
assert_eq!(
InputEncoding::Ascii.decode(&[0x80]),
Err(Error::Parse {
position: 0,
context: "source contains a non-ASCII byte"
})
);
assert_eq!(
InputEncoding::Utf16Le.decode(&[b'a', 0, b'b']),
Err(Error::Parse {
position: 2,
context: "UTF-16 source has a trailing byte"
})
);
}
#[test]
fn lexer_iterator_stops_after_a_diagnostic() {
let mut lexer = sample_lexer();
lexer
.start_with_string("alpha ! beta".to_owned())
.expect("source");
let mut iter = lexer.iter();
assert_eq!(
iter.next().expect("first").expect("token").yytext(),
"alpha"
);
assert!(matches!(
iter.next().expect("diagnostic"),
Err(Error::Parse {
position: 6,
context: "input does not match any lexer rule"
})
));
assert!(iter.next().is_none());
}
}