use anyhow::{anyhow, bail, Result};
use crate::ast::{Call, Expr, Pipeline, Receiver, Statement};
enum CallSyntax<'a> {
Paren { name: String, inner: &'a str },
Shorthand { name: String, inner: &'a str },
Bare { name: String },
}
pub fn parse_program(src: &str) -> Result<Vec<Pipeline>> {
let stmts = split_top_level(src, ';');
let mut out = Vec::new();
for stmt in stmts {
let s = stmt.trim();
if s.is_empty() {
continue;
}
out.push(parse_pipeline(s)?);
}
Ok(out)
}
fn parse_pipeline(src: &str) -> Result<Pipeline> {
let stages = split_pipeline_stages(src)
.into_iter()
.map(|stage| parse_statement(stage.trim()))
.collect::<Result<Vec<_>>>()?;
Ok(Pipeline { stages })
}
fn parse_statement(src: &str) -> Result<Statement> {
let (receiver, rest) = if let Some(rest) = src.strip_prefix("r.") {
(Receiver::Rec, rest)
} else if let Some(rest) = src.strip_prefix("rec.") {
(Receiver::Rec, rest)
} else if let Some(rest) = src.strip_prefix("g.") {
(Receiver::Grid, rest)
} else if let Some(rest) = src.strip_prefix("grid.") {
(Receiver::Grid, rest)
} else {
bail!("statement must start with r./rec. or g./grid.: {src}")
};
let parts = split_top_level(rest, '.');
let calls = parts
.into_iter()
.map(|part| parse_call(part.trim()))
.collect::<Result<Vec<_>>>()?;
Ok(Statement { receiver, calls })
}
fn split_pipeline_stages(src: &str) -> Vec<String> {
let mut out = Vec::new();
let mut depth = 0i32;
let mut in_str = false;
let mut cur = String::new();
let mut prev = '\0';
for (idx, ch) in src.char_indices() {
match ch {
'"' if prev != '\\' => {
in_str = !in_str;
cur.push(ch);
}
'(' if !in_str => {
depth += 1;
cur.push(ch);
}
')' if !in_str => {
depth -= 1;
cur.push(ch);
}
'|' if !in_str && depth == 0 && starts_with_receiver(&src[idx + ch.len_utf8()..]) => {
out.push(cur.trim().to_string());
cur.clear();
}
_ => cur.push(ch),
}
prev = ch;
}
if !cur.trim().is_empty() {
out.push(cur.trim().to_string());
}
out
}
fn starts_with_receiver(src: &str) -> bool {
let s = src.trim_start();
s.starts_with("r.") || s.starts_with("rec.") || s.starts_with("g.") || s.starts_with("grid.")
}
fn parse_call(src: &str) -> Result<Call> {
match parse_call_syntax(src)? {
CallSyntax::Bare { name } => Ok(Call {
name,
args: Vec::new(),
}),
CallSyntax::Paren { name, inner } | CallSyntax::Shorthand { name, inner } => {
let args = parse_args(inner)?;
Ok(Call { name, args })
}
}
}
fn parse_expr(src: &str) -> Result<Expr> {
let s = src.trim();
if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 {
return Ok(Expr::Str(
s[1..s.len() - 1].replace(r#"\""#, '"'.to_string().as_str()),
));
}
if s.starts_with('(') && s.ends_with(')') {
return parse_expr(&s[1..s.len() - 1]);
}
if is_call_syntax(s) {
return Ok(Expr::Call(parse_call(s)?));
}
if let Ok(n) = s.parse::<i64>() {
return Ok(Expr::Num(n));
}
Ok(Expr::Ident(s.to_string()))
}
fn is_call_syntax(src: &str) -> bool {
src.contains('(') || find_shorthand_delim(src).is_some()
}
fn parse_call_syntax(src: &str) -> Result<CallSyntax<'_>> {
let shorthand = find_shorthand_delim(src);
let open = src.find('(');
if let Some((idx, _delim)) = shorthand {
if open.map(|open_idx| idx < open_idx).unwrap_or(true) {
let name = src[..idx].trim().to_string();
ensure_call_name(&name, src)?;
return Ok(CallSyntax::Shorthand {
name,
inner: &src[idx + 1..],
});
}
}
if let Some(open_idx) = open {
let name = src[..open_idx].trim().to_string();
ensure_call_name(&name, src)?;
let close = src
.rfind(')')
.ok_or_else(|| anyhow!("expected ')' in call: {src}"))?;
return Ok(CallSyntax::Paren {
name,
inner: &src[open_idx + 1..close],
});
}
let name = src.trim().to_string();
ensure_call_name(&name, src)?;
Ok(CallSyntax::Bare { name })
}
fn parse_args(src: &str) -> Result<Vec<Expr>> {
if src.trim().is_empty() {
return Ok(Vec::new());
}
split_top_level(src, ',')
.into_iter()
.map(|arg| parse_expr(arg.trim()))
.collect()
}
fn find_shorthand_delim(src: &str) -> Option<(usize, char)> {
let colon = find_top_level(src, ':').map(|idx| (idx, ':'));
let equals = find_top_level(src, '=').map(|idx| (idx, '='));
match (colon, equals) {
(Some(c), Some(e)) => Some(if c.0 < e.0 { c } else { e }),
(Some(c), None) => Some(c),
(None, Some(e)) => Some(e),
(None, None) => None,
}
}
fn ensure_call_name(name: &str, src: &str) -> Result<()> {
if name.is_empty() {
bail!("empty call: {src}");
}
Ok(())
}
fn find_top_level(src: &str, target: char) -> Option<usize> {
let mut depth = 0i32;
let mut in_str = false;
let mut prev = '\0';
for (idx, ch) in src.char_indices() {
match ch {
'"' if prev != '\\' => in_str = !in_str,
'(' if !in_str => depth += 1,
')' if !in_str => depth -= 1,
_ if ch == target && !in_str && depth == 0 => return Some(idx),
_ => {}
}
prev = ch;
}
None
}
fn split_top_level(src: &str, delim: char) -> Vec<String> {
let mut out = Vec::new();
let mut depth = 0i32;
let mut in_str = false;
let mut cur = String::new();
let mut prev = '\0';
for ch in src.chars() {
match ch {
'"' if prev != '\\' => {
in_str = !in_str;
cur.push(ch);
}
'(' if !in_str => {
depth += 1;
cur.push(ch);
}
')' if !in_str => {
depth -= 1;
cur.push(ch);
}
_ if ch == delim && !in_str && depth == 0 => {
out.push(cur.trim().to_string());
cur.clear();
}
_ => cur.push(ch),
}
prev = ch;
}
if !cur.trim().is_empty() {
out.push(cur.trim().to_string());
}
out
}