Restructure and start working on CLI
This commit is contained in:
683
libjrpc/src/parser.rs
Normal file
683
libjrpc/src/parser.rs
Normal file
@ -0,0 +1,683 @@
|
||||
use anyhow::Result;
|
||||
use log::{debug, trace};
|
||||
use std::{collections::HashMap, error::Error, fmt::Display, sync::Arc};
|
||||
|
||||
use crate::{Token, TokenPosition, TokenType};
|
||||
|
||||
pub type PResult<T> = Result<T, ParserError>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParserPosition {
|
||||
path: Arc<String>,
|
||||
position: usize,
|
||||
token_positions: Vec<TokenPosition>,
|
||||
}
|
||||
|
||||
impl ParserPosition {
|
||||
pub fn from_token(token: &Token) -> Self {
|
||||
Self {
|
||||
path: token.2.path.clone(),
|
||||
position: token.2.start,
|
||||
token_positions: vec![token.2.clone()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Node {
|
||||
fn get_position(&self) -> ParserPosition;
|
||||
fn get_name(&self) -> String {
|
||||
String::from("")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RootNode {
|
||||
Define(DefineStatement),
|
||||
Import(ImportStatement),
|
||||
Service(ServiceStatement),
|
||||
Type(TypeStatement),
|
||||
Enum(EnumStatement),
|
||||
}
|
||||
|
||||
impl Node for RootNode {
|
||||
fn get_position(&self) -> ParserPosition {
|
||||
match self {
|
||||
RootNode::Define(stmt) => stmt.get_position(),
|
||||
RootNode::Import(stmt) => stmt.get_position(),
|
||||
RootNode::Service(stmt) => stmt.get_position(),
|
||||
RootNode::Type(stmt) => stmt.get_position(),
|
||||
RootNode::Enum(stmt) => stmt.get_position(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_name(&self) -> String {
|
||||
match self {
|
||||
RootNode::Define(_) => String::from("define"),
|
||||
RootNode::Import(_) => String::from("import"),
|
||||
RootNode::Service(_) => String::from("service"),
|
||||
RootNode::Type(_) => String::from("type"),
|
||||
RootNode::Enum(_) => String::from("enum"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DefineStatement {
|
||||
pub position: ParserPosition,
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
}
|
||||
impl Node for DefineStatement {
|
||||
fn get_position(&self) -> ParserPosition {
|
||||
self.position.clone()
|
||||
}
|
||||
fn get_name(&self) -> String {
|
||||
format!("define {}", self.key)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ImportStatement {
|
||||
pub position: ParserPosition,
|
||||
pub path: String,
|
||||
}
|
||||
impl Node for ImportStatement {
|
||||
fn get_position(&self) -> ParserPosition {
|
||||
self.position.clone()
|
||||
}
|
||||
|
||||
fn get_name(&self) -> String {
|
||||
format!("import {}", self.path)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TypeStatement {
|
||||
pub position: ParserPosition,
|
||||
pub name: String,
|
||||
pub fields: Vec<TypeFieldNode>,
|
||||
}
|
||||
impl Node for TypeStatement {
|
||||
fn get_position(&self) -> ParserPosition {
|
||||
self.position.clone()
|
||||
}
|
||||
|
||||
fn get_name(&self) -> String {
|
||||
format!("type {}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TypeFieldNode {
|
||||
pub position: ParserPosition,
|
||||
pub name: String,
|
||||
pub optional: bool,
|
||||
pub fieldtype: String,
|
||||
pub array: bool,
|
||||
pub map: Option<String>,
|
||||
}
|
||||
impl Node for TypeFieldNode {
|
||||
fn get_position(&self) -> ParserPosition {
|
||||
self.position.clone()
|
||||
}
|
||||
|
||||
fn get_name(&self) -> String {
|
||||
format!("type_field {}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EnumStatement {
|
||||
pub position: ParserPosition,
|
||||
pub name: String,
|
||||
pub values: Vec<EnumValueNode>,
|
||||
}
|
||||
impl Node for EnumStatement {
|
||||
fn get_position(&self) -> ParserPosition {
|
||||
self.position.clone()
|
||||
}
|
||||
|
||||
fn get_name(&self) -> String {
|
||||
format!("enum {}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EnumValueNode {
|
||||
pub position: ParserPosition,
|
||||
pub name: String,
|
||||
pub value: Option<i64>,
|
||||
}
|
||||
impl Node for EnumValueNode {
|
||||
fn get_position(&self) -> ParserPosition {
|
||||
self.position.clone()
|
||||
}
|
||||
|
||||
fn get_name(&self) -> String {
|
||||
format!("enum_field {}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ServiceStatement {
|
||||
pub position: ParserPosition,
|
||||
pub name: String,
|
||||
pub methods: Vec<ServiceMethodNode>,
|
||||
}
|
||||
impl Node for ServiceStatement {
|
||||
fn get_position(&self) -> ParserPosition {
|
||||
self.position.clone()
|
||||
}
|
||||
|
||||
fn get_name(&self) -> String {
|
||||
format!("service {}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ServiceMethodNode {
|
||||
pub position: ParserPosition,
|
||||
pub name: String,
|
||||
pub inputs: Vec<ServiceMethodInputNode>,
|
||||
pub return_type: Option<ServiceMethodReturnNode>,
|
||||
pub decorators: Vec<Decorator>,
|
||||
}
|
||||
impl Node for ServiceMethodNode {
|
||||
fn get_position(&self) -> ParserPosition {
|
||||
self.position.clone()
|
||||
}
|
||||
|
||||
fn get_name(&self) -> String {
|
||||
format!("method {}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Decorator {
|
||||
pub position: ParserPosition,
|
||||
pub name: String,
|
||||
pub args: Vec<String>,
|
||||
}
|
||||
impl Node for Decorator {
|
||||
fn get_position(&self) -> ParserPosition {
|
||||
self.position.clone()
|
||||
}
|
||||
|
||||
fn get_name(&self) -> String {
|
||||
format!("decorator {}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ServiceMethodInputNode {
|
||||
pub position: ParserPosition,
|
||||
pub name: String,
|
||||
pub fieldtype: String,
|
||||
pub optional: bool,
|
||||
pub array: bool,
|
||||
}
|
||||
impl Node for ServiceMethodInputNode {
|
||||
fn get_position(&self) -> ParserPosition {
|
||||
self.position.clone()
|
||||
}
|
||||
|
||||
fn get_name(&self) -> String {
|
||||
format!("method_arg {}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ServiceMethodReturnNode {
|
||||
pub position: ParserPosition,
|
||||
pub fieldtype: String,
|
||||
pub array: bool,
|
||||
}
|
||||
impl Node for ServiceMethodReturnNode {
|
||||
fn get_position(&self) -> ParserPosition {
|
||||
self.position.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Parser {
|
||||
tokens: Vec<Token>,
|
||||
position: usize,
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
pub fn new(tokens: Vec<Token>) -> Parser {
|
||||
// Remove all comments and spaces at this stage
|
||||
let tokens = tokens
|
||||
.into_iter()
|
||||
.filter(|e| e.0 != TokenType::Comment && e.0 != TokenType::Space)
|
||||
.collect();
|
||||
Parser {
|
||||
tokens,
|
||||
position: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn has_current_token(&self) -> bool {
|
||||
self.position < self.tokens.len()
|
||||
}
|
||||
|
||||
fn current_token(&mut self) -> &Token {
|
||||
&self.tokens[self.position]
|
||||
}
|
||||
|
||||
fn assert_types(&mut self, token_types: &[TokenType]) -> PResult<()> {
|
||||
if token_types.iter().any(|t| *t == self.current_token().0) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ParserError::new(
|
||||
&format!("Unexpected token. Expected on of {:?}", token_types),
|
||||
self.current_token(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn eat_token(&mut self) -> PResult<(String, ParserPosition)> {
|
||||
debug!("Parser::eat_token()");
|
||||
let pos = ParserPosition::from_token(self.current_token());
|
||||
let value = self.current_token().1.clone();
|
||||
self.position += 1;
|
||||
Ok((value, pos))
|
||||
}
|
||||
|
||||
fn eat_token_value(&mut self, value: &str) -> PResult<(String, ParserPosition)> {
|
||||
debug!("Parser::eat_token_value({})", value);
|
||||
let (val, pos) = self.eat_token()?;
|
||||
|
||||
if val != value {
|
||||
return Err(ParserError::new(
|
||||
&format!("Expected token of value {}", value),
|
||||
self.current_token(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok((val, pos))
|
||||
}
|
||||
|
||||
fn eat_token_type(&mut self, token_type: TokenType) -> PResult<(String, ParserPosition)> {
|
||||
debug!("Parser::eat_token_type({:?})", token_type);
|
||||
if self.current_token().0 != token_type {
|
||||
return Err(ParserError::new(
|
||||
&format!("Expected token of type {:?}", token_type),
|
||||
self.current_token(),
|
||||
));
|
||||
}
|
||||
|
||||
self.eat_token()
|
||||
}
|
||||
|
||||
fn eat_text(&mut self) -> PResult<(String, ParserPosition)> {
|
||||
debug!("Parser::eat_text()");
|
||||
self.eat_token_type(TokenType::Text)
|
||||
}
|
||||
|
||||
fn eat_text_with_keywords(&mut self) -> PResult<(String, ParserPosition)> {
|
||||
debug!("Parser::eat_text_with_keywords()");
|
||||
self.assert_types(&[TokenType::Text, TokenType::Keyword])?;
|
||||
self.eat_token()
|
||||
}
|
||||
|
||||
fn eat_string_or_text_as_raw(&mut self) -> PResult<String> {
|
||||
debug!("Parser::eat_string_or_text_as_raw()");
|
||||
self.assert_types(&[TokenType::String, TokenType::Text])?;
|
||||
let token = self.current_token();
|
||||
if token.0 == TokenType::String {
|
||||
let (value, _) = self.eat_token()?;
|
||||
let count = value.chars().count();
|
||||
Ok(value.chars().skip(1).take(count - 2).collect())
|
||||
} else {
|
||||
let (value, _) = self.eat_token()?;
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
fn eat_number(&mut self) -> PResult<i64> {
|
||||
debug!("Parser::eat_number()");
|
||||
self.assert_types(&[TokenType::Number])?;
|
||||
let (value, _) = self.eat_token()?;
|
||||
value
|
||||
.parse()
|
||||
.map_err(|_| ParserError::new("Invalid number", self.current_token()))
|
||||
}
|
||||
|
||||
fn parse_define(&mut self) -> PResult<DefineStatement> {
|
||||
debug!("Parser::parse_define()");
|
||||
let (_, position) = self.eat_token_value("define")?;
|
||||
|
||||
let (key, _) = self.eat_token()?;
|
||||
trace!("Parser::parse_define()::key = {}", key);
|
||||
|
||||
let value = self.eat_string_or_text_as_raw()?;
|
||||
|
||||
self.eat_token_type(TokenType::Semicolon)?;
|
||||
|
||||
Ok(DefineStatement {
|
||||
position,
|
||||
key,
|
||||
value,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_import(&mut self) -> PResult<ImportStatement> {
|
||||
debug!("Parser::parse_import()");
|
||||
let (_, pos) = self.eat_token_value("import")?;
|
||||
let path = self.eat_string_or_text_as_raw()?;
|
||||
|
||||
self.eat_token_type(TokenType::Semicolon)?;
|
||||
|
||||
Ok(ImportStatement {
|
||||
position: pos,
|
||||
path,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_enum_value(&mut self) -> PResult<EnumValueNode> {
|
||||
debug!("Parser::parse_enum_value()");
|
||||
let (name, pos) = self.eat_token()?;
|
||||
let value = if self.current_token().0 == TokenType::Equals {
|
||||
self.eat_token()?;
|
||||
Some(self.eat_number()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(EnumValueNode {
|
||||
position: pos,
|
||||
name,
|
||||
value,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_enum(&mut self) -> PResult<EnumStatement> {
|
||||
debug!("Parser::parse_enum()");
|
||||
let (_, pos) = self.eat_token_value("enum")?;
|
||||
let (name, _) = self.eat_token()?;
|
||||
trace!("Parser::parse_enum()::name = {}", name);
|
||||
|
||||
self.eat_token_type(TokenType::CurlyOpen)?;
|
||||
let mut values: Vec<EnumValueNode> = Vec::new();
|
||||
|
||||
while self.current_token().0 == TokenType::Text {
|
||||
values.push(self.parse_enum_value()?);
|
||||
if self.current_token().0 == TokenType::Comma {
|
||||
//TODO: Maybe use a next flag or break or something with the commas
|
||||
self.eat_token()?;
|
||||
}
|
||||
}
|
||||
|
||||
self.eat_token_type(TokenType::CurlyClose)?;
|
||||
|
||||
Ok(EnumStatement {
|
||||
position: pos,
|
||||
name,
|
||||
values,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_type_field(&mut self) -> PResult<TypeFieldNode> {
|
||||
debug!("Parser::parse_type_field()");
|
||||
let (name, pos) = self.eat_text_with_keywords()?;
|
||||
trace!("Parser::parse_type_field()::name = {}", name);
|
||||
|
||||
let mut optional = false;
|
||||
let mut array = false;
|
||||
let mut map = None;
|
||||
|
||||
if self.current_token().0 == TokenType::Questionmark {
|
||||
optional = true;
|
||||
self.eat_token()?;
|
||||
}
|
||||
|
||||
_ = self.eat_token_type(TokenType::Colon);
|
||||
let fieldtype = if self.current_token().0 == TokenType::CurlyOpen {
|
||||
self.eat_token()?;
|
||||
let (key_type, _) = self.eat_text()?;
|
||||
self.eat_token_type(TokenType::Comma)?;
|
||||
let (value_type, _) = self.eat_text()?;
|
||||
self.eat_token_type(TokenType::CurlyClose)?;
|
||||
map = Some(key_type);
|
||||
value_type
|
||||
} else {
|
||||
let (type_name, _) = self.eat_text()?;
|
||||
if self.current_token().0 == TokenType::Array {
|
||||
array = true;
|
||||
self.eat_token()?;
|
||||
}
|
||||
|
||||
type_name
|
||||
};
|
||||
|
||||
self.eat_token_type(TokenType::Semicolon)?;
|
||||
|
||||
Ok(TypeFieldNode {
|
||||
position: pos,
|
||||
name,
|
||||
optional,
|
||||
fieldtype,
|
||||
map,
|
||||
array,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_type(&mut self) -> PResult<TypeStatement> {
|
||||
debug!("Parser::parse_type()");
|
||||
let (_, pos) = self.eat_token_value("type")?;
|
||||
let (name, _) = self.eat_text()?;
|
||||
trace!("Parser::prase_type()::name = {}", name);
|
||||
self.eat_token_type(TokenType::CurlyOpen)?;
|
||||
|
||||
let mut fields = Vec::new();
|
||||
while self.current_token().0 == TokenType::Text
|
||||
|| self.current_token().0 == TokenType::Keyword
|
||||
{
|
||||
fields.push(self.parse_type_field()?);
|
||||
}
|
||||
|
||||
self.eat_token_type(TokenType::CurlyClose)?;
|
||||
|
||||
Ok(TypeStatement {
|
||||
position: pos,
|
||||
name,
|
||||
fields,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_decorator(&mut self) -> PResult<Decorator> {
|
||||
debug!("Parser::parse_decorator()");
|
||||
let (_, position) = self.eat_token_type(TokenType::At)?;
|
||||
let (decorator, _) = self.eat_text()?;
|
||||
trace!("Parser::parse_decorator()::name = {}", decorator);
|
||||
|
||||
self.eat_token_type(TokenType::BracketOpen)?;
|
||||
|
||||
let mut args = Vec::new();
|
||||
let mut first = true;
|
||||
while self.current_token().0 != TokenType::BracketClose {
|
||||
if first {
|
||||
first = false
|
||||
} else {
|
||||
self.eat_token_type(TokenType::Comma)?;
|
||||
}
|
||||
|
||||
args.push(self.eat_string_or_text_as_raw()?);
|
||||
}
|
||||
self.eat_token_type(TokenType::BracketClose)?;
|
||||
|
||||
Ok(Decorator {
|
||||
name: decorator,
|
||||
args,
|
||||
position,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_method(
|
||||
&mut self,
|
||||
decorators: Vec<Decorator>,
|
||||
notification: bool,
|
||||
) -> PResult<ServiceMethodNode> {
|
||||
debug!(
|
||||
"Parser::parse_method({}, {})",
|
||||
decorators.len(),
|
||||
notification
|
||||
);
|
||||
let (name, pos) = self.eat_text()?;
|
||||
trace!("Parser::parse_method()::name = {}", name);
|
||||
|
||||
self.eat_token_type(TokenType::BracketOpen)?;
|
||||
|
||||
let mut inputs = Vec::new();
|
||||
|
||||
while self.current_token().0 != TokenType::BracketClose {
|
||||
let (name, position) = self.eat_text_with_keywords()?;
|
||||
|
||||
let mut optional = false;
|
||||
if self.current_token().0 == TokenType::Questionmark {
|
||||
optional = true;
|
||||
self.eat_token()?;
|
||||
}
|
||||
|
||||
self.eat_token_type(TokenType::Colon)?;
|
||||
let (fieldtype, _) = self.eat_text()?;
|
||||
let mut array = false;
|
||||
if self.current_token().0 == TokenType::Array {
|
||||
array = true;
|
||||
self.eat_token()?;
|
||||
}
|
||||
inputs.push(ServiceMethodInputNode {
|
||||
name,
|
||||
fieldtype,
|
||||
array,
|
||||
optional,
|
||||
position,
|
||||
});
|
||||
|
||||
if self.current_token().0 == TokenType::Comma {
|
||||
self.eat_token()?;
|
||||
}
|
||||
|
||||
trace!(
|
||||
"Parser::parse_method()::params_next_token: {}",
|
||||
self.current_token().1
|
||||
);
|
||||
}
|
||||
|
||||
self.eat_token_type(TokenType::BracketClose)?;
|
||||
|
||||
let mut return_type = None;
|
||||
if !notification {
|
||||
self.eat_token_type(TokenType::Colon)?;
|
||||
|
||||
let (fieldtype, position) = self.eat_text()?;
|
||||
let mut array = false;
|
||||
if self.current_token().0 == TokenType::Array {
|
||||
array = true;
|
||||
self.eat_token()?;
|
||||
}
|
||||
|
||||
return_type = Some(ServiceMethodReturnNode {
|
||||
position,
|
||||
fieldtype,
|
||||
array,
|
||||
});
|
||||
}
|
||||
|
||||
self.eat_token_type(TokenType::Semicolon)?;
|
||||
|
||||
Ok(ServiceMethodNode {
|
||||
position: pos,
|
||||
name,
|
||||
inputs,
|
||||
return_type,
|
||||
decorators,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_service(&mut self) -> PResult<ServiceStatement> {
|
||||
debug!("Parser::parse_service()");
|
||||
let (_, pos) = self.eat_token_value("service")?;
|
||||
let (name, _) = self.eat_text()?;
|
||||
trace!("Parser::parse_service()::name = {}", name);
|
||||
|
||||
self.eat_token_type(TokenType::CurlyOpen)?;
|
||||
|
||||
let mut methods = Vec::new();
|
||||
while self.current_token().0 != TokenType::CurlyClose {
|
||||
let mut decorators = Vec::new();
|
||||
while self.current_token().0 == TokenType::At {
|
||||
decorators.push(self.parse_decorator()?);
|
||||
}
|
||||
|
||||
let mut notification = false;
|
||||
if self.current_token().1 == "notification" {
|
||||
self.eat_token()?;
|
||||
notification = true;
|
||||
}
|
||||
|
||||
methods.push(self.parse_method(decorators, notification)?);
|
||||
}
|
||||
|
||||
self.eat_token_type(TokenType::CurlyClose)?;
|
||||
|
||||
Ok(ServiceStatement {
|
||||
position: pos,
|
||||
name,
|
||||
methods,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_statement(&mut self) -> PResult<RootNode> {
|
||||
debug!("Parser::parse_statement()");
|
||||
let token = self.current_token();
|
||||
|
||||
if let TokenType::Keyword = token.0 {
|
||||
trace!("Parser::parse_statement()::type = {}", token.1);
|
||||
return match token.1.as_str() {
|
||||
"define" => Ok(RootNode::Define(self.parse_define()?)),
|
||||
"import" => Ok(RootNode::Import(self.parse_import()?)),
|
||||
"type" => Ok(RootNode::Type(self.parse_type()?)),
|
||||
"service" => Ok(RootNode::Service(self.parse_service()?)),
|
||||
"enum" => Ok(RootNode::Enum(self.parse_enum()?)),
|
||||
_ => Err(ParserError::new("Unknown keyword", token)),
|
||||
};
|
||||
} else {
|
||||
Err(ParserError::new("Expected keyword", token))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(&mut self) -> PResult<Vec<RootNode>> {
|
||||
debug!("Parser::parse()");
|
||||
let mut nodes = Vec::new();
|
||||
|
||||
while self.has_current_token() {
|
||||
nodes.push(self.parse_statement()?);
|
||||
}
|
||||
|
||||
Ok(nodes)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParserError {
|
||||
pub message: String,
|
||||
pub token: Token,
|
||||
}
|
||||
|
||||
impl ParserError {
|
||||
fn new(msg: &str, token: &Token) -> ParserError {
|
||||
ParserError {
|
||||
message: format!("{}: {}", msg, token.1),
|
||||
token: token.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for ParserError {}
|
||||
impl Display for ParserError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "ParserError: {} at {:?}", self.message, self.token)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user