Add initial parsing

Signed-off-by: Marcel Müller <neikos@neikos.email>
This commit is contained in:
Marcel Müller 2025-06-29 12:07:07 +02:00
parent 12327bb44c
commit 87db209786
6 changed files with 244 additions and 2 deletions

49
src/cli.rs Normal file
View file

@ -0,0 +1,49 @@
use camino::Utf8PathBuf;
use clap::Parser;
#[derive(Debug, Parser)]
pub struct Args {
#[clap(short, long)]
pub expression: String,
#[clap(short, long, default_value_t = Utf8PathBuf::from("-"))]
input: Utf8PathBuf,
#[clap(short, long, group = "delimiters")]
lines: bool,
#[clap(short, long, group = "delimiters", default_value_t = true)]
words: bool,
}
pub enum InputDelimiter {
Lines,
Words,
}
pub enum Input {
Stdin,
FilePath(Utf8PathBuf),
}
impl Args {
pub fn delimiter(&self) -> InputDelimiter {
if self.lines {
return InputDelimiter::Lines;
}
if self.words {
return InputDelimiter::Words;
}
unreachable!("Either lines or words has to be true")
}
pub fn input(&self) -> Input {
if self.input == "-" {
Input::Stdin
} else {
Input::FilePath(self.input.clone())
}
}
}

173
src/expr.rs Normal file
View file

@ -0,0 +1,173 @@
use std::ops::Range;
use miette::LabeledSpan;
use winnow::LocatingSlice;
use winnow::ModalResult;
use winnow::Parser;
use winnow::ascii::escaped;
use winnow::ascii::space0;
use winnow::ascii::space1;
use winnow::combinator::alt;
use winnow::combinator::delimited;
use winnow::combinator::separated;
use winnow::error::ContextError;
use winnow::error::StrContext;
use winnow::token::none_of;
pub fn parse(src: &str) -> miette::Result<TokenList<'_>> {
Ok(TokenList {
expressions: parse_program
.parse(LocatingSlice::new(src))
.map_err(|parse_error| {
let labels = vec![LabeledSpan::new(
Some("Here".to_string()),
parse_error.offset(),
0,
)];
miette::diagnostic!(
labels = labels,
"Could not parse expression: {:?}",
parse_error
)
})?,
src,
})
}
type Input<'p> = LocatingSlice<&'p str>;
type Error = ContextError;
#[derive(Debug, Clone)]
pub struct TokenList<'p> {
expressions: Vec<Token<'p>>,
src: &'p str,
}
#[derive(Debug, Clone)]
pub struct Token<'p> {
span: Range<usize>,
expr: Tok<'p>,
}
#[derive(Debug, Clone)]
pub enum Tok<'p> {
Print,
PerWord,
PerLine,
Match,
FunctionApplication,
String(String),
Dummy(&'p ()),
}
fn parse_program<'p>(input: &mut Input<'p>) -> ModalResult<Vec<Token<'p>>, Error> {
delimited(space0, separated(1.., parse_expression, space1), space0)
.context(StrContext::Label("program"))
.parse_next(input)
}
fn parse_expression<'p>(input: &mut Input<'p>) -> ModalResult<Token<'p>, Error> {
alt((parse_builtin, parse_string))
.context(StrContext::Label("expression"))
.parse_next(input)
}
fn parse_builtin<'p>(input: &mut Input<'p>) -> ModalResult<Token<'p>, Error> {
alt((
"@w".value(Tok::PerWord)
.context(winnow::error::StrContext::Label("@w")),
"@l".value(Tok::PerLine)
.context(winnow::error::StrContext::Label("@l")),
"print"
.value(Tok::Print)
.context(winnow::error::StrContext::Label("print")),
"match"
.value(Tok::Match)
.context(winnow::error::StrContext::Label("match")),
"|".value(Tok::FunctionApplication)
.context(winnow::error::StrContext::Label("|")),
))
.with_span()
.map(|(expr, span)| Token { expr, span })
.parse_next(input)
}
fn parse_string<'p>(input: &mut Input<'p>) -> ModalResult<Token<'p>, Error> {
let content = escaped(
none_of(['\'', '\\']),
'\\',
alt(("\\".value("\\"), "\'".value("\'"))),
)
.map(Tok::String);
delimited('\'', content, '\'')
.with_span()
.map(|(expr, span)| Token { expr, span })
.parse_next(input)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn check_simple_print() {
let input = "@w print";
let expr = parse(input).unwrap();
insta::assert_debug_snapshot!(expr, @r#"
TokenList {
expressions: [
Token {
span: 0..2,
expr: PerWord,
},
Token {
span: 3..8,
expr: Print,
},
],
src: "@w print",
}
"#);
}
#[test]
fn check_complex_print() {
let input = "@l match 'foo' | print";
let expr = parse(input).unwrap();
insta::assert_debug_snapshot!(expr, @r#"
TokenList {
expressions: [
Token {
span: 0..2,
expr: PerLine,
},
Token {
span: 3..8,
expr: Match,
},
Token {
span: 9..14,
expr: String(
"foo",
),
},
Token {
span: 15..16,
expr: FunctionApplication,
},
Token {
span: 17..22,
expr: Print,
},
],
src: "@l match 'foo' | print",
}
"#);
}
}

View file

@ -1,3 +1,14 @@
fn main() {
println!("Hello, world!");
use clap::Parser;
mod cli;
mod expr;
fn main() -> miette::Result<()> {
let args = cli::Args::parse();
let input_delim = args.delimiter();
let input = args.input();
let expression = expr::parse(&args.expression)?;
Ok(())
}