rkg 0.1.0

A one-liner oriented record/grid processor
use anyhow::{anyhow, bail, Result};

use crate::ast::{Call, Expr, Receiver, Statement};

pub fn parse_program(src: &str) -> Result<Vec<Statement>> {
    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_statement(s)?);
    }
    Ok(out)
}

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("d.") {
        (Receiver::Grid, rest)
    } else if let Some(rest) = src.strip_prefix("grid.") {
        (Receiver::Grid, rest)
    } else {
        bail!("statement must start with r./rec. or d./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 parse_call(src: &str) -> Result<Call> {
    let open = src
        .find('(')
        .ok_or_else(|| anyhow!("expected '(' in call: {src}"))?;
    let close = src
        .rfind(')')
        .ok_or_else(|| anyhow!("expected ')' in call: {src}"))?;
    let name = src[..open].trim().to_string();
    let inner = &src[open + 1..close];
    let args = if inner.trim().is_empty() {
        Vec::new()
    } else {
        split_top_level(inner, ',')
            .into_iter()
            .map(|arg| parse_expr(arg.trim()))
            .collect::<Result<Vec<_>>>()?
    };

    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 s.contains('(') && s.ends_with(')') {
        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 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
}