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,
open_parens: 0,
depth: 0,
};
p.skip_space();
if p.pos >= p.src.len() {
return Err(in_expression(src, p.error("empty expression")));
}
let e = p.parse_binary(0).map_err(|e| in_expression(src, e))?;
p.skip_space();
if p.pos < p.src.len() {
return Err(in_expression(src, p.after_expression()));
}
Ok(e)
}
fn in_expression(src: &str, mut err: ParseError) -> ParseError {
let marked = if err.msg.ends_with("at _@_") {
let at = char_boundary(src, err.offset.min(src.len()));
format!("{}_@_{}", &src[..at], &src[at..])
} else {
src.to_string()
};
let hint = err
.msg
.strip_prefix("invalid bareword \"")
.and_then(|rest| rest.strip_suffix('"'))
.map(|w| format!("should be \"${w}\" or \"{{{w}}}\" or \"{w}(...)\" or ..."));
err.msg = match hint {
Some(hint) => format!("{}\nin expression \"{marked}\";\n{hint}", err.msg),
None => format!("{}\nin expression \"{marked}\"", err.msg),
};
err
}
fn char_boundary(src: &str, at: usize) -> usize {
let mut at = at;
while at > 0 && !src.is_char_boundary(at) {
at -= 1;
}
at
}
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)],
];
fn is_operator_char(c: char) -> bool {
matches!(
c,
'+' | '-'
| '*'
| '/'
| '%'
| '~'
| '!'
| '&'
| '|'
| '^'
| '<'
| '>'
| '='
| '?'
| ':'
| ','
)
}
fn starts_an_operand(c: char) -> bool {
c.is_ascii_digit()
|| c.is_ascii_alphabetic()
|| matches!(c, '.' | '_' | '$' | '[' | '"' | '{' | '(')
}
fn is_word_operator(name: &str) -> bool {
LEVELS
.iter()
.flat_map(|level| level.iter())
.any(|(text, _)| *text == name && text.bytes().all(|b| b.is_ascii_alphabetic()))
}
struct ExprParser<'a> {
src: &'a str,
pos: usize,
open_parens: 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 char_here(&self) -> char {
self.src[self.pos..]
.chars()
.next()
.expect("a byte at the cursor is part of a character")
}
fn missing_operand(&self) -> ParseError {
if self.pos >= self.src.len() {
return self.error(if self.open_parens > 0 {
"unbalanced open paren"
} else {
"missing operand at _@_"
});
}
match self.char_here() {
')' => self.error("unbalanced close paren"),
'=' if self.bytes().get(self.pos + 1) != Some(&b'=') => {
self.error("incomplete operator \"=\"")
}
c if is_operator_char(c) => self.error("missing operand at _@_"),
c => self.error(&format!("invalid character \"{c}\"")),
}
}
fn after_expression(&self) -> ParseError {
match self.char_here() {
')' => self.error("unbalanced close paren"),
c if is_operator_char(c) => self.error("missing operand at _@_"),
c if c.is_ascii_alphabetic() => {
let b = self.bytes();
let mut end = self.pos;
while end < b.len() && (b[end].is_ascii_alphanumeric() || b[end] == b'_') {
end += 1;
}
let word = &self.src[self.pos..end];
if crate::runtime::boolean_word(word).is_some() {
return self.error("missing operator at _@_");
}
self.error(&format!("invalid bareword {word:?}"))
}
c if starts_an_operand(c) => self.error("missing operator at _@_"),
c => self.error(&format!("invalid character \"{c}\"")),
}
}
fn skip_space(&mut self) {
loop {
match self.peek() {
Some(b' ' | b'\t' | b'\n' | b'\r') => self.pos += 1,
Some(b'#') => {
while !matches!(self.peek(), None | Some(b'\n')) {
self.pos += 1;
}
}
_ => return,
}
}
}
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 operator \":\" at _@_"));
}
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.missing_operand()),
Some(b'(') => {
self.pos += 1;
self.open_parens += 1;
let e = self.nested(|p| p.parse_binary(0))?;
self.skip_space();
if self.peek() != Some(b')') {
return Err(self.error("unbalanced open paren"));
}
self.pos += 1;
self.open_parens -= 1;
Ok(e)
}
Some(b'$') => {
let Some((part, next)) = parser::substitution_at(self.src, self.pos)? else {
return Err(self.error("invalid character \"$\""));
};
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() => self.parse_call(),
Some(_) => Err(self.missing_operand()),
}
}
fn digit_run(&self, start: usize) -> Result<usize, usize> {
let b = self.bytes();
let mut end = start;
while end < b.len() && (b[end].is_ascii_digit() || b[end] == b'_') {
end += 1;
}
if end > start && b[start] == b'_' {
return Err(start);
}
if end > start && b[end - 1] == b'_' {
return Err(end - 1);
}
Ok(end)
}
fn separator_error(&self, at: usize, dot_led: bool) -> ParseError {
if dot_led {
return self.error("invalid character \".\"");
}
let b = self.bytes();
let is_word = |c: u8| c.is_ascii_alphanumeric() || c == b'_';
let mut lo = at;
while lo > 0 && is_word(b[lo - 1]) {
lo -= 1;
}
let mut hi = at;
while hi < b.len() && is_word(b[hi]) {
hi += 1;
}
self.error(&format!("invalid bareword {:?}", &self.src[lo..hi]))
}
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();
let dot_led = b[start] == b'.';
end = self
.digit_run(end)
.map_err(|at| self.separator_error(at, dot_led))?;
let mut is_float = false;
if end < b.len() && b[end] == b'.' {
is_float = true;
end += 1;
let fraction = end;
end = self
.digit_run(end)
.map_err(|at| self.separator_error(at, dot_led || at == fraction))?;
}
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() || b[probe] == b'_') {
is_float = true;
end = self
.digit_run(probe)
.map_err(|at| self.separator_error(at, dot_led))?;
}
}
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(|_| {
if dot_led {
self.error("invalid character \".\"")
} else {
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();
if written.starts_with('_') {
return Err(self.separator_error(self.pos + prefix_len, false));
}
if written.ends_with('_') {
return Err(self.separator_error(self.pos + prefix_len + written.len() - 1, false));
}
let digits: String = written.chars().filter(|c| *c != '_').collect();
if digits.is_empty() {
let word: String = self.src[self.pos..]
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
return Err(self.error(&format!("invalid bareword {word:?}")));
}
let text: Box<str> =
format!("{}{written}", &self.src[self.pos..self.pos + prefix_len]).into();
self.pos += prefix_len + written.len();
Ok(match i64::from_str_radix(&digits, radix) {
Ok(v) => Expr::Int(v, text),
Err(_) => Expr::Subst(vec![Part::Lit(text.to_string())]),
})
}
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();
if let Ok(f) = name.parse::<f64>() {
self.pos = end;
return Ok(Expr::Float(f, name.into()));
}
self.pos = end;
self.skip_space();
if self.peek() != Some(b'(') {
if is_word_operator(&name) {
self.pos = start;
return Err(self.error("missing operand at _@_"));
}
if crate::runtime::boolean_word(&name).is_some() {
return Ok(Expr::Subst(vec![Part::Lit(name)]));
}
return Err(self.error(&format!("invalid bareword {name:?}")));
}
self.pos += 1;
self.open_parens += 1;
let mut args = Vec::new();
self.skip_space();
if self.peek() == Some(b')') {
self.pos += 1;
self.open_parens -= 1;
return Ok(Expr::Call(name, args));
}
if self.peek() == Some(b',') {
return Err(self.error("missing function argument at _@_"));
}
loop {
args.push(self.nested(|p| p.parse_binary(0))?);
self.skip_space();
match self.peek() {
Some(b',') => {
self.pos += 1;
self.skip_space();
if self.peek().is_none() || self.peek() == Some(b')') {
return Err(self.error("missing function argument at _@_"));
}
}
Some(b')') => {
self.pos += 1;
self.open_parens -= 1;
return Ok(Expr::Call(name, args));
}
None => return Err(self.error("unbalanced open paren")),
Some(_) => return Err(self.error("missing operator at _@_")),
}
}
}
}