use kaish_types::plan::{Expansion, FragmentAddr, Hole};
use kaish_types::Value;
use crate::arithmetic::{ArithExpr, Expansion as ArithExpansion};
use crate::ast::plan::{heredoc_targets, plan_statement, render_expr};
use crate::ast::{Expr, Stmt, StringPart, VarPath, VarSegment};
use crate::interpreter::Evaluator;
use crate::interpreter::Scope;
use crate::parser::{self, ParseError};
#[derive(Debug)]
#[non_exhaustive]
pub enum FragmentError {
Parse(Vec<ParseError>),
NoSuchStatement { asked: usize, statements: usize },
NoSuchHeredoc { asked: usize, heredocs: usize },
NeedsSessionState { what: String },
Eval { message: String },
}
impl std::fmt::Display for FragmentError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Parse(errors) => {
write!(f, "source does not parse: {} error(s)", errors.len())
}
Self::NoSuchStatement { asked, statements } => write!(
f,
"no statement {asked}: the program has {statements}"
),
Self::NoSuchHeredoc { asked, heredocs } => {
write!(f, "no heredoc {asked}: the statement has {heredocs}")
}
Self::NeedsSessionState { what } => write!(
f,
"body reads {what}, which a supplied scope cannot carry — expand it in a kernel that holds the session instead"
),
Self::Eval { message } => write!(f, "cannot expand body: {message}"),
}
}
}
impl std::error::Error for FragmentError {}
pub fn expand_fragment(
source: &str,
addr: FragmentAddr,
scope: &[(String, Value)],
) -> Result<Expansion, FragmentError> {
let program = parser::parse(source).map_err(FragmentError::Parse)?;
let planned: Vec<&Stmt> = program
.statements
.iter()
.filter(|stmt| !matches!(stmt, Stmt::Empty))
.collect();
let stmt = *planned
.get(addr.statement)
.ok_or(FragmentError::NoSuchStatement {
asked: addr.statement,
statements: planned.len(),
})?;
let targets = heredoc_targets(stmt);
let target = targets.get(addr.heredoc).ok_or(FragmentError::NoSuchHeredoc {
asked: addr.heredoc,
heredocs: targets.len(),
})?;
expand_target(target, scope)
}
fn expand_target(target: &Expr, scope: &[(String, Value)]) -> Result<Expansion, FragmentError> {
let mut holes = Vec::new();
collect_holes(target, &mut holes);
if !holes.is_empty() {
return Ok(Expansion::Blocked { holes });
}
if let Some(what) = session_state_read(target) {
return Err(FragmentError::NeedsSessionState { what });
}
let mut session = Scope::new();
for (name, value) in scope {
session.set(name.clone(), value.clone());
}
let value = Evaluator::new(&mut session)
.eval(target)
.map_err(|e| FragmentError::Eval {
message: e.to_string(),
})?;
match value {
Value::String(text) => Ok(Expansion::Complete(text)),
other => Err(FragmentError::Eval {
message: format!("body evaluated to {other:?} instead of text"),
}),
}
}
fn collect_holes(expr: &Expr, out: &mut Vec<Hole>) {
match expr {
Expr::HereDocBody { parts, .. } => {
for part in parts {
part_holes(&part.part, out);
}
}
Expr::Interpolated(parts) => {
for part in parts {
part_holes(part, out);
}
}
Expr::CommandSubst(stmts) => out.push(hole(expr, stmts)),
_ => {}
}
}
fn part_holes(part: &StringPart, out: &mut Vec<Hole>) {
match part {
StringPart::CommandSubst(stmts) => {
out.push(hole(&Expr::CommandSubst(stmts.clone()), stmts))
}
StringPart::VarWithDefault { default, .. } => {
for part in default {
part_holes(part, out);
}
}
StringPart::Arithmetic(expr) => arithmetic_holes(expr, out),
_ => {}
}
}
fn arithmetic_holes(expr: &str, out: &mut Vec<Hole>) {
if let Ok(parsed) = crate::arithmetic::parse(expr) {
arith_expr_holes(&parsed, out);
}
}
fn arith_expr_holes(expr: &ArithExpr, out: &mut Vec<Hole>) {
match expr {
ArithExpr::Int(_) => {}
ArithExpr::Expansion(e) => arith_expansion_holes(e, out),
ArithExpr::Subscript { indices, .. } => {
for index in indices {
arith_expr_holes(index, out);
}
}
ArithExpr::BasedExpansion { expansion, .. } => arith_expansion_holes(expansion, out),
ArithExpr::Unary { operand, .. } => arith_expr_holes(operand, out),
ArithExpr::Binary { left, right, .. } => {
arith_expr_holes(left, out);
arith_expr_holes(right, out);
}
ArithExpr::Ternary { cond, then_branch, else_branch } => {
arith_expr_holes(cond, out);
arith_expr_holes(then_branch, out);
arith_expr_holes(else_branch, out);
}
}
}
fn arith_expansion_holes(e: &ArithExpansion, out: &mut Vec<Hole>) {
match e {
ArithExpansion::CommandSubst(stmts) => {
out.push(hole(&Expr::CommandSubst(stmts.clone()), stmts))
}
ArithExpansion::Nested(inner) => arith_expr_holes(inner, out),
ArithExpansion::BracedDefault { default, .. } => arithmetic_holes(default, out),
ArithExpansion::Var(_) | ArithExpansion::BracedPath { .. }
| ArithExpansion::LastExitCode | ArithExpansion::CurrentPid => {}
}
}
fn hole(expr: &Expr, stmts: &[Stmt]) -> Hole {
let plans = stmts
.iter()
.filter(|s| !matches!(s, Stmt::Empty))
.map(|s| plan_statement(s).plan)
.collect();
Hole::new(render_expr(expr), plans)
}
fn session_state_read(expr: &Expr) -> Option<String> {
let parts: &[_] = match expr {
Expr::HereDocBody { parts, .. } => return parts.iter().find_map(|p| part_state(&p.part)),
Expr::Interpolated(parts) => parts,
_ => return None,
};
parts.iter().find_map(part_state)
}
fn part_state(part: &StringPart) -> Option<String> {
match part {
StringPart::LastExitCode => Some("$?".to_string()),
StringPart::CurrentPid => Some("$$".to_string()),
StringPart::Positional(n) => Some(format!("${n}")),
StringPart::AllArgs => Some("$@".to_string()),
StringPart::ArgCount => Some("$#".to_string()),
StringPart::VarWithDefault { path, default } => var_path_state(path)
.or_else(|| default.iter().find_map(part_state)),
StringPart::Arithmetic(expr) => arithmetic_state(expr),
StringPart::Var(path) | StringPart::VarLength(path) => var_path_state(path),
_ => None,
}
}
fn var_path_state(path: &VarPath) -> Option<String> {
match path.segments.first()? {
VarSegment::Field(name) if name == "?" => Some("${?}".to_string()),
_ => None,
}
}
fn arithmetic_state(expr: &str) -> Option<String> {
let parsed = crate::arithmetic::parse(expr).ok()?;
arith_expr_state(&parsed)
}
fn arith_expr_state(expr: &ArithExpr) -> Option<String> {
match expr {
ArithExpr::Int(_) => None,
ArithExpr::Expansion(e) => arith_expansion_state(e),
ArithExpr::Subscript { indices, .. } => indices.iter().find_map(arith_expr_state),
ArithExpr::BasedExpansion { expansion, .. } => arith_expansion_state(expansion),
ArithExpr::Unary { operand, .. } => arith_expr_state(operand),
ArithExpr::Binary { left, right, .. } => {
arith_expr_state(left).or_else(|| arith_expr_state(right))
}
ArithExpr::Ternary { cond, then_branch, else_branch } => arith_expr_state(cond)
.or_else(|| arith_expr_state(then_branch))
.or_else(|| arith_expr_state(else_branch)),
}
}
fn arith_expansion_state(e: &ArithExpansion) -> Option<String> {
match e {
ArithExpansion::LastExitCode => Some("$?".to_string()),
ArithExpansion::CurrentPid => Some("$$".to_string()),
ArithExpansion::Var(name) if name.parse::<usize>().is_ok() => Some(format!("${name}")),
ArithExpansion::Var(_) | ArithExpansion::BracedPath { .. } => None,
ArithExpansion::BracedDefault { default, .. } => arithmetic_state(default),
ArithExpansion::CommandSubst(_) => None,
ArithExpansion::Nested(inner) => arith_expr_state(inner),
}
}