use crate::parser::{self, ParseError, Part};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnOp {
Neg,
Plus,
BitNot,
Not,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BinOp {
Pow,
Mul,
Div,
Mod,
Add,
Sub,
Shl,
Shr,
Lt,
Gt,
Le,
Ge,
StrLt,
StrGt,
StrLe,
StrGe,
Eq,
Ne,
StrEq,
StrNe,
In,
Ni,
BitAnd,
BitXor,
BitOr,
And,
Or,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Expr {
Int(i64, Box<str>),
Float(f64, Box<str>),
Subst(Vec<Part>),
Unary(UnOp, Box<Expr>),
Binary(BinOp, Box<Expr>, Box<Expr>),
Ternary(Box<Expr>, Box<Expr>, Box<Expr>),
Call(String, Vec<Expr>),
}
pub const MAX_EXPR_DEPTH: usize = 5_000;
pub fn parse(src: &str) -> Result<Expr, ParseError> {
let mut p = ExprParser {
src,
pos: 0,
depth: 0,
};
p.skip_space();
let e = p.parse_binary(0)?;
p.skip_space();
if p.pos < p.src.len() {
return Err(p.error(&format!(
"extra characters after expression: {:?}",
&p.src[p.pos..]
)));
}
Ok(e)
}
pub const LEVELS: &[&[(&str, BinOp)]] = &[
&[("||", BinOp::Or)],
&[("&&", BinOp::And)],
&[("|", BinOp::BitOr)],
&[("^", BinOp::BitXor)],
&[("&", BinOp::BitAnd)],
&[
("==", BinOp::Eq),
("!=", BinOp::Ne),
("eq", BinOp::StrEq),
("ne", BinOp::StrNe),
("in", BinOp::In),
("ni", BinOp::Ni),
],
&[
("<=", BinOp::Le),
(">=", BinOp::Ge),
("<", BinOp::Lt),
(">", BinOp::Gt),
("lt", BinOp::StrLt),
("gt", BinOp::StrGt),
("le", BinOp::StrLe),
("ge", BinOp::StrGe),
],
&[("<<", BinOp::Shl), (">>", BinOp::Shr)],
&[("+", BinOp::Add), ("-", BinOp::Sub)],
&[("*", BinOp::Mul), ("/", BinOp::Div), ("%", BinOp::Mod)],
&[("**", BinOp::Pow)],
];
struct ExprParser<'a> {
src: &'a str,
pos: usize,
depth: usize,
}
impl<'a> ExprParser<'a> {
fn bytes(&self) -> &'a [u8] {
self.src.as_bytes()
}
fn nested<T>(
&mut self,
parse: impl FnOnce(&mut Self) -> Result<T, ParseError>,
) -> Result<T, ParseError> {
if self.depth >= MAX_EXPR_DEPTH {
return Err(self.error("too many nested subexpressions (infinite loop?)"));
}
self.depth += 1;
let parsed = parse(self);
self.depth -= 1;
parsed
}
fn peek(&self) -> Option<u8> {
self.bytes().get(self.pos).copied()
}
fn error(&self, msg: &str) -> ParseError {
ParseError {
msg: msg.to_string(),
offset: self.pos,
line: 1,
}
}
fn skip_space(&mut self) {
while matches!(self.peek(), Some(b' ' | b'\t' | b'\n' | b'\r')) {
self.pos += 1;
}
}
fn match_op(&self, op: &str) -> bool {
if !self.src[self.pos..].starts_with(op) {
return false;
}
if op.as_bytes()[0].is_ascii_alphabetic() {
match self.bytes().get(self.pos + op.len()) {
Some(b) if b.is_ascii_alphanumeric() || *b == b'_' => return false,
_ => {}
}
}
if op == "*" && self.src[self.pos..].starts_with("**") {
return false;
}
if op == "<" && self.src[self.pos..].starts_with("<<") {
return false;
}
if op == ">" && self.src[self.pos..].starts_with(">>") {
return false;
}
if (op == "&" && self.src[self.pos..].starts_with("&&"))
|| (op == "|" && self.src[self.pos..].starts_with("||"))
{
return false;
}
true
}
fn parse_binary(&mut self, level: usize) -> Result<Expr, ParseError> {
if level >= LEVELS.len() {
return self.parse_unary();
}
let mut lhs = self.parse_binary(level + 1)?;
loop {
self.skip_space();
let Some(&(tok, op)) = LEVELS[level].iter().find(|(tok, _)| self.match_op(tok)) else {
break;
};
self.pos += tok.len();
self.skip_space();
let rhs = if op == BinOp::Pow {
self.nested(|p| p.parse_binary(level))?
} else {
self.parse_binary(level + 1)?
};
lhs = Expr::Binary(op, Box::new(lhs), Box::new(rhs));
}
if level == 0 {
self.skip_space();
if self.peek() == Some(b'?') {
self.pos += 1;
self.skip_space();
let then = self.nested(|p| p.parse_binary(0))?;
self.skip_space();
if self.peek() != Some(b':') {
return Err(self.error("missing : in ternary"));
}
self.pos += 1;
self.skip_space();
let other = self.nested(|p| p.parse_binary(0))?;
lhs = Expr::Ternary(Box::new(lhs), Box::new(then), Box::new(other));
}
}
Ok(lhs)
}
fn parse_unary(&mut self) -> Result<Expr, ParseError> {
self.skip_space();
let op = match self.peek() {
Some(b'-') => Some(UnOp::Neg),
Some(b'+') => Some(UnOp::Plus),
Some(b'~') => Some(UnOp::BitNot),
Some(b'!') if self.bytes().get(self.pos + 1) != Some(&b'=') => Some(UnOp::Not),
_ => None,
};
if let Some(op) = op {
self.pos += 1;
let operand = self.nested(|p| p.parse_unary())?;
return Ok(Expr::Unary(op, Box::new(operand)));
}
self.parse_operand()
}
fn parse_operand(&mut self) -> Result<Expr, ParseError> {
self.skip_space();
match self.peek() {
None => Err(self.error("premature end of expression")),
Some(b'(') => {
self.pos += 1;
let e = self.nested(|p| p.parse_binary(0))?;
self.skip_space();
if self.peek() != Some(b')') {
return Err(self.error("missing close-paren"));
}
self.pos += 1;
Ok(e)
}
Some(b'$') => {
let Some((part, next)) = parser::substitution_at(self.src, self.pos)? else {
return Err(self.error("invalid $ in expression"));
};
self.pos = next;
Ok(Expr::Subst(vec![part]))
}
Some(b'[') => {
let (script, next) = parser::command_at(self.src, self.pos)?;
self.pos = next;
Ok(Expr::Subst(vec![Part::Script(script)]))
}
Some(b'"') => {
let (parts, next) = parser::quoted_at(self.src, self.pos)?;
self.pos = next;
Ok(Expr::Subst(parts))
}
Some(b'{') => {
let (text, next) = parser::braced_at(self.src, self.pos)?;
self.pos = next;
Ok(Expr::Subst(vec![Part::Lit(text)]))
}
Some(b) if b.is_ascii_digit() || b == b'.' => self.parse_number(),
Some(b) if b.is_ascii_alphabetic() || b == b'_' => self.parse_call(),
Some(_) => {
let c = self.src[self.pos..]
.chars()
.next()
.expect("a byte at the cursor is part of a character");
Err(self.error(&format!("invalid character \"{c}\"")))
}
}
}
fn parse_number(&mut self) -> Result<Expr, ParseError> {
let start = self.pos;
let rest = &self.src[start..];
if let Some(radix_body) = rest.strip_prefix("0x").or_else(|| rest.strip_prefix("0X")) {
return self.radix_literal(radix_body, 16, 2);
}
if let Some(radix_body) = rest.strip_prefix("0o").or_else(|| rest.strip_prefix("0O")) {
return self.radix_literal(radix_body, 8, 2);
}
if let Some(radix_body) = rest.strip_prefix("0b").or_else(|| rest.strip_prefix("0B")) {
return self.radix_literal(radix_body, 2, 2);
}
if let Some(radix_body) = rest.strip_prefix("0d").or_else(|| rest.strip_prefix("0D")) {
return self.radix_literal(radix_body, 10, 2);
}
let mut end = start;
let b = self.bytes();
while end < b.len() && (b[end].is_ascii_digit() || b[end] == b'_') {
end += 1;
}
let mut is_float = false;
if end < b.len() && b[end] == b'.' {
is_float = true;
end += 1;
while end < b.len() && b[end].is_ascii_digit() {
end += 1;
}
}
if end < b.len() && (b[end] == b'e' || b[end] == b'E') {
let mut probe = end + 1;
if probe < b.len() && (b[probe] == b'+' || b[probe] == b'-') {
probe += 1;
}
if probe < b.len() && b[probe].is_ascii_digit() {
is_float = true;
end = probe;
while end < b.len() && b[end].is_ascii_digit() {
end += 1;
}
}
}
let text = &self.src[start..end];
self.pos = end;
let bare: String = text.chars().filter(|c| *c != '_').collect();
if is_float {
bare.parse::<f64>()
.map(|v| Expr::Float(v, text.into()))
.map_err(|_| self.error(&format!("invalid floating-point number {text:?}")))
} else {
bare.parse::<i64>()
.map(|v| Expr::Int(v, text.into()))
.or_else(|_| Ok(Expr::Subst(vec![Part::Lit(text.to_string())])))
}
}
fn radix_literal(
&mut self,
body: &str,
radix: u32,
prefix_len: usize,
) -> Result<Expr, ParseError> {
let written: String = body
.chars()
.take_while(|c| c.is_digit(radix) || *c == '_')
.collect();
let digits: String = written.chars().filter(|c| *c != '_').collect();
if digits.is_empty() {
return Err(self.error("missing digits after radix prefix"));
}
let text: Box<str> = format!("{}{written}", &self.src[self.pos..self.pos + prefix_len]).into();
self.pos += prefix_len + written.len();
i64::from_str_radix(&digits, radix)
.map(|v| Expr::Int(v, text))
.map_err(|_| self.error("integer value too large to represent"))
}
fn parse_call(&mut self) -> Result<Expr, ParseError> {
let start = self.pos;
let b = self.bytes();
let mut end = start;
while end < b.len() && (b[end].is_ascii_alphanumeric() || b[end] == b'_' || b[end] == b':')
{
end += 1;
}
let name = self.src[start..end].to_string();
self.pos = end;
self.skip_space();
if self.peek() != Some(b'(') {
return Err(self.error(&format!("invalid bare word {name:?} in expression")));
}
self.pos += 1;
let mut args = Vec::new();
self.skip_space();
if self.peek() == Some(b')') {
self.pos += 1;
return Ok(Expr::Call(name, args));
}
loop {
args.push(self.nested(|p| p.parse_binary(0))?);
self.skip_space();
match self.peek() {
Some(b',') => {
self.pos += 1;
}
Some(b')') => {
self.pos += 1;
return Ok(Expr::Call(name, args));
}
_ => return Err(self.error("missing close-paren in function call")),
}
}
}
}