use kaish_types::plan::{Expansion, FragmentAddr, Hole};
use kaish_types::Value;
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);
}
}
_ => {}
}
}
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 chars: Vec<char> = expr.chars().collect();
let skip_spaces = |mut i: usize| {
while chars.get(i).is_some_and(|c| c.is_whitespace()) {
i += 1;
}
i
};
for (i, c) in chars.iter().enumerate() {
if *c != '$' {
continue;
}
let mut j = skip_spaces(i + 1);
if chars.get(j) == Some(&'{') {
j = skip_spaces(j + 1);
}
match chars.get(j) {
Some(c) if c.is_alphabetic() || *c == '_' => {}
None => {}
Some(c) => return Some(format!("${c}")),
}
}
None
}