use std::collections::HashMap;
use super::pattern::SedRegex;
use super::script::{Addr, CaseOp, EndAddr, Kind, Program, RepPart, StartAddr, Subst};
use crate::builtins::limits::SED_MAX_CYCLE_STEPS;
use crate::limits::{BudgetedString, ExecutionBudget, LimitExceeded};
pub(super) struct InputLine {
pub(super) text: String,
pub(super) had_newline: bool,
pub(super) file: usize,
}
pub(super) struct Sink {
pub(super) buf: BudgetedString,
pub(super) error: Option<LimitExceeded>,
missing_newline: bool,
sep: char,
}
impl Sink {
fn with_sep(sep: char, budget: Option<&ExecutionBudget>) -> Self {
let (buf, error) = match BudgetedString::new(budget) {
Ok(buf) => (buf, None),
Err(error) => (
BudgetedString::new(None).expect("an unbudgeted empty string is infallible"),
Some(error),
),
};
Sink {
buf,
error,
missing_newline: false,
sep,
}
}
}
impl Sink {
fn line(&mut self, text: &str, had_newline: bool) {
self.flush_pending();
self.push_str(text);
if had_newline {
self.push(self.sep);
} else {
self.missing_newline = true;
}
}
fn raw(&mut self, text: &str) {
self.flush_pending();
self.push_str(text);
}
fn flush_pending(&mut self) {
if self.missing_newline {
self.push(self.sep);
self.missing_newline = false;
}
}
fn push_str(&mut self, text: &str) {
if self.error.is_some() {
return;
}
if let Err(error) = self.buf.try_push_str(text) {
self.error = Some(error);
}
}
fn push(&mut self, ch: char) {
let mut encoded = [0; 4];
self.push_str(ch.encode_utf8(&mut encoded));
}
}
#[derive(Default, Clone)]
struct RangeState {
active: bool,
end_line: Option<usize>,
start_spent: bool,
}
enum Append {
Text(String),
File(String),
FileLine(String),
}
enum Cycle {
Auto,
Silent,
Restart,
Break,
}
pub(super) struct Machine<'a> {
prog: &'a Program,
names: &'a [String],
read_files: &'a HashMap<String, String>,
quiet: bool,
default_line_len: usize,
sep: char,
execution_budget: Option<ExecutionBudget>,
out: Sink,
pub(super) stderr: String,
pub(super) write_files: HashMap<String, Sink>,
range: Vec<RangeState>,
hold: BudgetedString,
hold_had_newline: bool,
appends: Vec<Append>,
read_cursor: HashMap<String, usize>,
last_regex: Option<std::sync::Arc<SedRegex>>,
pub(super) exit_code: Option<i32>,
warned: bool,
flush_newline_on_quit: bool,
lines: &'a [InputLine],
cursor: usize,
line_no: usize,
ps: BudgetedString,
ps_had_newline: bool,
cur_file: usize,
sub_made: bool,
}
struct WriteHandles<'a> {
sep: char,
out: &'a mut Sink,
err: &'a mut String,
files: &'a mut HashMap<String, Sink>,
budget: Option<&'a ExecutionBudget>,
}
impl<'a> Machine<'a> {
pub(super) fn new(
prog: &'a Program,
names: &'a [String],
read_files: &'a HashMap<String, String>,
quiet: bool,
line_len: usize,
sep: char,
execution_budget: Option<ExecutionBudget>,
) -> Result<Self, LimitExceeded> {
let out = Sink::with_sep(sep, execution_budget.as_ref());
if let Some(budget) = &execution_budget {
budget.check()?;
}
let hold = BudgetedString::new(execution_budget.as_ref())?;
let ps = BudgetedString::new(execution_budget.as_ref())?;
Ok(Machine {
prog,
names,
read_files,
quiet: quiet || prog.quiet,
default_line_len: line_len,
sep,
out,
stderr: String::new(),
write_files: HashMap::new(),
range: Vec::new(),
hold,
hold_had_newline: true,
appends: Vec::new(),
read_cursor: HashMap::new(),
last_regex: None,
exit_code: None,
warned: false,
flush_newline_on_quit: false,
lines: &[],
cursor: 0,
line_no: 0,
ps,
execution_budget,
ps_had_newline: true,
cur_file: 0,
sub_made: false,
})
}
pub(super) fn finished(&self) -> bool {
self.exit_code.is_some()
}
fn space_fail(&mut self, error: LimitExceeded) -> Cycle {
if self.out.error.is_none() {
self.out.error = Some(error);
}
self.warned = true;
Cycle::Break
}
fn fresh_space(&self) -> Result<BudgetedString, LimitExceeded> {
match &self.execution_budget {
Some(budget) => {
budget.check()?;
BudgetedString::new(Some(budget))
}
None => BudgetedString::new(None),
}
}
fn take_ps(&mut self) -> Result<BudgetedString, LimitExceeded> {
let fresh = self.fresh_space()?;
Ok(std::mem::replace(&mut self.ps, fresh))
}
fn set_ps(&mut self, text: &str) -> Result<(), LimitExceeded> {
self.ps = self.fresh_space()?;
self.ps.try_push_str(text)
}
pub(super) fn run_segment(&mut self, lines: &'a [InputLine]) -> Result<String, LimitExceeded> {
self.lines = lines;
self.cursor = 0;
self.line_no = 0;
self.range = vec![RangeState::default(); self.prog.cmds.len()];
self.out = Sink::with_sep(self.sep, self.execution_budget.as_ref());
let mut restart = false;
let mut restarts = 0usize;
let mut restart_cursor = self.cursor;
loop {
if let Some(budget) = &self.execution_budget
&& let Err(error) = budget.check()
{
return Err(error);
}
if self.exit_code.is_some() {
break;
}
if restart {
restarts += 1;
if restarts > SED_MAX_CYCLE_STEPS {
self.warn_loop_limit();
break;
}
if self.cursor != restart_cursor {
restarts = 0;
restart_cursor = self.cursor;
}
} else {
if !self.read_next() {
break;
}
restarts = 0;
restart_cursor = self.cursor;
}
restart = false;
match self.cycle() {
Cycle::Break => {
break;
}
Cycle::Auto => {
if !self.quiet {
self.out.line(&self.ps, self.ps_had_newline);
}
self.flush_appends();
}
Cycle::Silent => self.flush_appends(),
Cycle::Restart => {
self.flush_appends();
restart = true;
}
}
if self.flush_newline_on_quit {
self.out.flush_pending();
}
}
let replacement = Sink::with_sep(self.sep, self.execution_budget.as_ref());
let out = std::mem::replace(&mut self.out, replacement);
match out.error {
Some(error) => Err(error),
None => Ok(out.buf.into_inner()),
}
}
fn cycle(&mut self) -> Cycle {
let cmds = &self.prog.cmds;
let mut pc = 0usize;
let mut steps = 0usize;
let mut step_cursor = self.cursor;
while pc < cmds.len() {
if self.cursor != step_cursor {
step_cursor = self.cursor;
steps = 0;
}
if let Some(budget) = &self.execution_budget
&& let Err(error) = budget.check().and_then(|()| budget.consume_work(1))
{
return self.space_fail(error);
}
steps += 1;
if steps > SED_MAX_CYCLE_STEPS {
self.warn_loop_limit();
break;
}
let cmd = &cmds[pc];
let (matched, range_ended) = match &cmd.addr {
None => (true, false),
Some(addr) => self.address_matches(pc, addr),
};
let apply = if cmd.negate { !matched } else { matched };
match &cmd.kind {
Kind::Block(end) => {
pc = if apply { pc + 1 } else { *end + 1 };
continue;
}
Kind::BlockEnd | Kind::Label(_) | Kind::Nop => {
pc += 1;
continue;
}
_ => {}
}
if !apply {
pc += 1;
continue;
}
match &cmd.kind {
Kind::Branch(label) => match label {
None => return Cycle::Auto,
Some(name) => {
pc = self.label_pc(name);
continue;
}
},
Kind::BranchIfSub(label) => {
if self.sub_made {
self.sub_made = false;
match label {
None => return Cycle::Auto,
Some(name) => {
pc = self.label_pc(name);
continue;
}
}
}
}
Kind::BranchIfNoSub(label) => {
if self.sub_made {
self.sub_made = false;
} else {
match label {
None => return Cycle::Auto,
Some(name) => {
pc = self.label_pc(name);
continue;
}
}
}
}
Kind::Substitute(subst) => {
if let Err(error) = self.substitute(subst) {
return self.space_fail(error);
}
}
Kind::Transliterate(map) => {
let mut translated = match BudgetedString::new(self.execution_budget.as_ref()) {
Ok(buf) => buf,
Err(error) => return self.space_fail(error),
};
let mut failed = None;
for c in self.ps.chars() {
let mapped = map
.iter()
.find(|(from, _)| *from == c)
.map_or(c, |(_, to)| *to);
if let Err(error) = translated.try_push(mapped) {
failed = Some(error);
break;
}
}
if let Some(error) = failed {
return self.space_fail(error);
}
self.ps = translated;
}
Kind::Delete => return Cycle::Silent,
Kind::DeleteFirstLine => {
return match self.ps.find(self.sep) {
Some(idx) => {
let start = idx + self.sep.len_utf8();
let mut rest = match BudgetedString::new(self.execution_budget.as_ref())
{
Ok(buf) => buf,
Err(error) => return self.space_fail(error),
};
if let Err(error) = rest.try_push_str(&self.ps[start..]) {
return self.space_fail(error);
}
self.ps = rest;
Cycle::Restart
}
None => Cycle::Silent,
};
}
Kind::Print => {
self.out.line(&self.ps, self.ps_had_newline);
}
Kind::PrintFirstLine => match self.ps.find(self.sep) {
Some(idx) => {
let end = idx;
self.out.line(&self.ps[..end], true);
}
None => self.out.line(&self.ps, self.ps_had_newline),
},
Kind::Next => {
if !self.quiet {
self.out.line(&self.ps, self.ps_had_newline);
}
self.flush_appends();
if !self.read_next() {
self.exit_code = Some(0);
return Cycle::Silent;
}
}
Kind::NextAppend => {
self.flush_appends();
if self.cursor >= self.lines.len() {
self.exit_code = Some(0);
return Cycle::Auto;
}
let line = &self.lines[self.cursor];
if let Err(error) = self
.ps
.try_push(self.sep)
.and_then(|()| self.ps.try_push_str(&line.text))
{
return self.space_fail(error);
}
if let Some(budget) = &self.execution_budget
&& let Err(error) = budget.consume_input(line.text.len())
{
return self.space_fail(error);
}
self.ps_had_newline = line.had_newline;
self.cur_file = line.file;
self.cursor += 1;
self.line_no += 1;
self.sub_made = false;
}
Kind::Quit(code) => {
self.exit_code = Some(*code);
self.flush_newline_on_quit = true;
return Cycle::Auto;
}
Kind::QuitSilent(code) => {
self.exit_code = Some(*code);
self.appends.clear();
return Cycle::Silent;
}
Kind::Append(text) => self.appends.push(Append::Text(text.clone())),
Kind::Insert(text) => {
let text = format!("{text}\n");
self.out.raw(&text);
}
Kind::Change(text) => {
let ranged = cmd.addr.as_ref().is_some_and(|a| a.end.is_some());
if !ranged || cmd.negate || range_ended {
let text = format!("{text}\n");
self.out.raw(&text);
}
return Cycle::Silent;
}
Kind::HoldCopy => {
self.hold = match BudgetedString::new(self.execution_budget.as_ref()) {
Ok(buf) => buf,
Err(error) => return self.space_fail(error),
};
if let Err(error) = self.hold.try_push_str(&self.ps) {
return self.space_fail(error);
}
self.hold_had_newline = self.ps_had_newline;
}
Kind::HoldAppend => {
if let Err(error) = self
.hold
.try_push(self.sep)
.and_then(|()| self.hold.try_push_str(&self.ps))
{
return self.space_fail(error);
}
self.hold_had_newline = self.ps_had_newline;
}
Kind::GetCopy => {
self.ps = match BudgetedString::new(self.execution_budget.as_ref()) {
Ok(buf) => buf,
Err(error) => return self.space_fail(error),
};
if let Err(error) = self.ps.try_push_str(&self.hold) {
return self.space_fail(error);
}
self.ps_had_newline = self.hold_had_newline;
}
Kind::GetAppend => {
if let Err(error) = self
.ps
.try_push(self.sep)
.and_then(|()| self.ps.try_push_str(&self.hold))
{
return self.space_fail(error);
}
self.ps_had_newline = self.hold_had_newline;
}
Kind::Exchange => {
std::mem::swap(&mut self.ps, &mut self.hold);
std::mem::swap(&mut self.ps_had_newline, &mut self.hold_had_newline);
}
Kind::LineNumber => {
let text = format!("{}\n", self.line_no);
self.out.raw(&text);
}
Kind::List(width) => {
let width = width.unwrap_or(self.default_line_len);
let text = format!("{}\n", list_format(&self.ps, width));
self.out.raw(&text);
}
Kind::ReadFile(name) => self.appends.push(Append::File(name.clone())),
Kind::ReadLine(name) => self.appends.push(Append::FileLine(name.clone())),
Kind::WriteFile(name) => {
let nl = self.ps_had_newline;
let mut handles = WriteHandles {
sep: self.sep,
out: &mut self.out,
err: &mut self.stderr,
files: &mut self.write_files,
budget: self.execution_budget.as_ref(),
};
Self::write_out(&mut handles, name, &self.ps, nl);
}
Kind::WriteFirstLine(name) => match self.ps.find('\n') {
Some(idx) => {
let end = idx;
let mut handles = WriteHandles {
sep: self.sep,
out: &mut self.out,
err: &mut self.stderr,
files: &mut self.write_files,
budget: self.execution_budget.as_ref(),
};
Self::write_out(&mut handles, name, &self.ps[..end], true);
}
None => {
let nl = self.ps_had_newline;
let mut handles = WriteHandles {
sep: self.sep,
out: &mut self.out,
err: &mut self.stderr,
files: &mut self.write_files,
budget: self.execution_budget.as_ref(),
};
Self::write_out(&mut handles, name, &self.ps, nl);
}
},
Kind::Zap => match self.fresh_space() {
Ok(buf) => self.ps = buf,
Err(error) => return self.space_fail(error),
},
Kind::FileName => {
let name = self
.names
.get(self.cur_file)
.cloned()
.unwrap_or_else(|| "-".to_string());
let text = format!("{name}\n");
self.out.raw(&text);
}
Kind::Block(_) | Kind::BlockEnd | Kind::Label(_) | Kind::Nop => {}
}
pc += 1;
}
Cycle::Auto
}
fn warn_loop_limit(&mut self) {
if self.warned {
return;
}
self.warned = true;
self.stderr.push_str(&format!(
"sed: warning: branch/label loop limit ({SED_MAX_CYCLE_STEPS}) reached on line {}; \
output may be truncated\n",
self.line_no
));
}
fn read_next(&mut self) -> bool {
if self.cursor >= self.lines.len() {
return false;
}
let line = &self.lines[self.cursor];
let _scratch = match &self.execution_budget {
Some(budget) => match budget.lease_bytes(line.text.len()) {
Ok(lease) => Some(lease),
Err(error) => {
if self.out.error.is_none() {
self.out.error = Some(error);
}
self.warned = true;
return false;
}
},
None => None,
};
let text = line.text.clone();
let had_newline = line.had_newline;
let file = line.file;
if let Some(budget) = &self.execution_budget
&& let Err(error) = budget.consume_input(text.len())
{
if self.out.error.is_none() {
self.out.error = Some(error);
}
self.warned = true;
return false;
}
if let Err(error) = self.set_ps(&text) {
if self.out.error.is_none() {
self.out.error = Some(error);
}
self.warned = true;
return false;
}
self.ps_had_newline = had_newline;
self.cur_file = file;
self.cursor += 1;
self.line_no += 1;
self.sub_made = false;
true
}
fn label_pc(&self, target: &str) -> usize {
self.prog
.cmds
.iter()
.position(|c| matches!(&c.kind, Kind::Label(name) if name == target))
.unwrap_or(self.prog.cmds.len())
}
fn write_out(h: &mut WriteHandles<'_>, name: &str, text: &str, had_newline: bool) {
match name {
"/dev/stdout" => h.out.line(text, had_newline),
"/dev/stderr" => {
h.err.push_str(text);
if had_newline {
h.err.push('\n');
}
}
_ => h
.files
.entry(name.to_string())
.or_insert_with(|| Sink::with_sep(h.sep, h.budget))
.line(text, had_newline),
}
}
fn flush_appends(&mut self) {
if self.appends.is_empty() {
return;
}
for item in std::mem::take(&mut self.appends) {
match item {
Append::Text(text) => {
let text = format!("{text}\n");
self.out.raw(&text);
}
Append::File(name) => {
let Some(content) = self.read_files.get(&name) else {
continue;
};
if content.is_empty() {
continue;
}
match content.strip_suffix('\n') {
Some(body) => self.out.line(body, true),
None => self.out.line(content, false),
}
}
Append::FileLine(name) => {
let Some(content) = self.read_files.get(&name) else {
continue;
};
let offset = self.read_cursor.entry(name.clone()).or_insert(0);
if *offset >= content.len() {
continue;
}
let rest = &content[*offset..];
match rest.find('\n') {
Some(idx) => {
let line = rest[..idx].to_string();
*offset += idx + 1;
self.out.line(&line, true);
}
None => {
let line = rest.to_string();
*offset = content.len();
self.out.line(&line, false);
}
}
}
}
}
}
fn resolve(
&mut self,
re: &Option<std::sync::Arc<SedRegex>>,
) -> Option<std::sync::Arc<SedRegex>> {
match re {
Some(re) => {
self.last_regex = Some(re.clone());
Some(re.clone())
}
None => self.last_regex.clone(),
}
}
fn is_last_line(&self) -> bool {
self.cursor >= self.lines.len()
}
fn address_matches(&mut self, pc: usize, addr: &Addr) -> (bool, bool) {
let Some(end) = &addr.end else {
return (self.start_matches(&addr.start), false);
};
let state = self.range[pc].clone();
if state.active {
let (closed, selected) = match end {
EndAddr::Line(n) => (self.line_no >= *n, self.line_no <= *n),
EndAddr::Plus(_) | EndAddr::Multiple(_) => match state.end_line {
Some(e) => (self.line_no >= e, self.line_no <= e),
None => (false, true),
},
EndAddr::Last => (self.is_last_line(), true),
EndAddr::Regex(re) => {
let re = self.resolve(re);
(re.is_some_and(|re| re.is_match(&self.ps)), true)
}
};
if closed {
let relative = matches!(end, EndAddr::Plus(_) | EndAddr::Multiple(_));
self.range[pc] = RangeState {
start_spent: state.start_spent && (selected || !relative),
..RangeState::default()
};
}
if selected || !matches!(end, EndAddr::Plus(_) | EndAddr::Multiple(_)) {
return (selected, selected && closed);
}
}
let state = self.range[pc].clone();
let one_shot = matches!(addr.start, StartAddr::Line(_) | StartAddr::Zero);
let starts = match &addr.start {
StartAddr::Line(n) => !state.start_spent && self.line_no >= *n,
StartAddr::Zero => !state.start_spent,
other => self.start_matches(other),
};
if !starts {
return (false, false);
}
if one_shot {
self.range[pc].start_spent = true;
}
let end_line = match end {
EndAddr::Line(n) => Some(*n),
EndAddr::Plus(n) => Some(self.line_no + n),
EndAddr::Multiple(n) => Some(if *n <= 1 {
self.line_no
} else {
(self.line_no / n + 1) * n
}),
EndAddr::Last | EndAddr::Regex(_) => None,
};
if let Some(e) = end_line
&& e <= self.line_no
{
return (true, true);
}
if matches!(end, EndAddr::Last) && self.is_last_line() {
return (true, true);
}
if let EndAddr::Regex(re) = end
&& matches!(addr.start, StartAddr::Zero)
{
let re = self.resolve(re);
if re.is_some_and(|re| re.is_match(&self.ps)) {
return (true, true);
}
}
self.range[pc] = RangeState {
active: true,
end_line,
start_spent: one_shot,
};
(true, false)
}
fn start_matches(&mut self, start: &StartAddr) -> bool {
match start {
StartAddr::Line(n) => self.line_no == *n,
StartAddr::Zero => false,
StartAddr::Last => self.is_last_line(),
StartAddr::Step(first, step) => {
if *step == 0 {
self.line_no == *first
} else if *first == 0 {
self.line_no.is_multiple_of(*step)
} else {
self.line_no >= *first && (self.line_no - *first).is_multiple_of(*step)
}
}
StartAddr::Regex(re) => {
let re = self.resolve(re);
re.is_some_and(|re| re.is_match(&self.ps))
}
}
}
fn substitute(&mut self, subst: &Subst) -> Result<(), LimitExceeded> {
let Some(re) = self.resolve(&subst.re) else {
return Ok(());
};
let spans = re.matches(&self.ps);
if spans.len() < subst.occurrence {
return Ok(());
}
let old = self.take_ps()?;
let mut out = BudgetedString::new(self.execution_budget.as_ref())?;
let mut cursor = 0usize;
let mut replaced = false;
for (index, span) in spans.iter().enumerate() {
let nth = index + 1;
if nth < subst.occurrence || (nth > subst.occurrence && !subst.global) {
continue;
}
out.try_push_str(&old[cursor..span.start])?;
expand(&subst.parts, &old, span, &mut out)?;
cursor = span.end;
replaced = true;
}
if !replaced {
self.ps = old;
return Ok(());
}
out.try_push_str(&old[cursor..])?;
self.ps = out;
self.sub_made = true;
if subst.print {
self.out.line(&self.ps, self.ps_had_newline);
}
if let Some(name) = &subst.wfile {
let nl = self.ps_had_newline;
let mut handles = WriteHandles {
sep: self.sep,
out: &mut self.out,
err: &mut self.stderr,
files: &mut self.write_files,
budget: self.execution_budget.as_ref(),
};
Self::write_out(&mut handles, name, &self.ps, nl);
}
Ok(())
}
}
#[derive(Default)]
struct CaseState {
span: Option<bool>,
one: Option<bool>,
}
fn push_cased(
out: &mut BudgetedString,
text: &str,
state: &mut CaseState,
) -> Result<(), LimitExceeded> {
for ch in text.chars() {
if let Some(upper) = state.one.take() {
let mapped: Vec<char> = if upper {
ch.to_uppercase().collect()
} else {
ch.to_lowercase().collect()
};
for c in mapped {
out.try_push(c)?;
}
continue;
}
match state.span {
Some(true) => {
for c in ch.to_uppercase() {
out.try_push(c)?;
}
}
Some(false) => {
for c in ch.to_lowercase() {
out.try_push(c)?;
}
}
None => out.try_push(ch)?,
}
}
Ok(())
}
fn expand(
parts: &[RepPart],
subject: &str,
span: &super::pattern::MatchSpan,
out: &mut BudgetedString,
) -> Result<(), LimitExceeded> {
let mut state = CaseState::default();
for part in parts {
match part {
RepPart::Lit(text) => push_cased(out, text, &mut state)?,
RepPart::Group(0) => push_cased(out, &subject[span.start..span.end], &mut state)?,
RepPart::Group(n) => {
if let Some(Some((s, e))) = span.groups.get(n - 1) {
push_cased(out, &subject[*s..*e], &mut state)?;
}
}
RepPart::Case(op) => match op {
CaseOp::Upper => {
state.span = Some(true);
state.one = None;
}
CaseOp::Lower => {
state.span = Some(false);
state.one = None;
}
CaseOp::UpperOne => state.one = Some(true),
CaseOp::LowerOne => state.one = Some(false),
CaseOp::End => {
state.span = None;
state.one = None;
}
},
}
}
Ok(())
}
pub(super) fn list_format(text: &str, width: usize) -> String {
let mut out = String::new();
let mut column = 0usize;
for ch in text.chars() {
let token = match ch {
'\\' => "\\\\".to_string(),
'\x07' => "\\a".to_string(),
'\x08' => "\\b".to_string(),
'\x0c' => "\\f".to_string(),
'\n' => "\\n".to_string(),
'\r' => "\\r".to_string(),
'\t' => "\\t".to_string(),
'\x0b' => "\\v".to_string(),
c if c.is_ascii_graphic() || c == ' ' => c.to_string(),
c => {
let mut buf = [0u8; 4];
c.encode_utf8(&mut buf)
.as_bytes()
.iter()
.map(|b| format!("\\{b:03o}"))
.collect()
}
};
if width > 1 && column + token.len() > width - 1 {
out.push_str("\\\n");
column = 0;
}
column += token.len();
out.push_str(&token);
}
out.push('$');
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::limits::ExecutionLimits;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
fn parse_script(script: &str) -> Program {
match super::super::script::parse_with(script, false, false) {
Ok(prog) => prog,
Err(_) => panic!("test script parses"),
}
}
fn test_program() -> Program {
parse_script("p")
}
fn test_empty_program() -> Program {
parse_script("")
}
fn test_lines() -> Vec<InputLine> {
vec![InputLine {
text: "x".to_string(),
had_newline: true,
file: 0,
}]
}
fn test_machine(
prog: &Program,
budget: Option<ExecutionBudget>,
) -> Result<Machine<'_>, LimitExceeded> {
use std::sync::OnceLock;
static EMPTY: OnceLock<HashMap<String, String>> = OnceLock::new();
let files = EMPTY.get_or_init(HashMap::new);
Machine::new(prog, &[], files, true, 0, '\n', budget)
}
#[test]
fn cancelled_budget_refuses_segment() {
let prog = test_program();
let cancelled = Arc::new(AtomicBool::new(true));
let budget = ExecutionBudget::new(&ExecutionLimits::new(), cancelled);
assert!(
test_machine(&prog, Some(budget)).is_err(),
"dead budget must refuse construction"
);
}
#[test]
fn cancelled_budget_refuses_empty_program() {
let prog = test_empty_program();
let cancelled = Arc::new(AtomicBool::new(true));
let budget = ExecutionBudget::new(&ExecutionLimits::new(), cancelled);
assert!(
test_machine(&prog, Some(budget)).is_err(),
"dead budget must refuse construction"
);
}
#[test]
fn live_budget_runs_segment() {
let prog = test_program();
let budget =
ExecutionBudget::new(&ExecutionLimits::new(), Arc::new(AtomicBool::new(false)));
let mut machine = match test_machine(&prog, Some(budget)) {
Ok(machine) => machine,
Err(error) => panic!("live budget must construct: {error:?}"), };
let result = machine.run_segment(&test_lines());
assert!(result.is_ok(), "live budget must run, got {result:?}"); }
fn many_x_lines(count: usize) -> Vec<InputLine> {
(0..count)
.map(|_| InputLine {
text: "x".to_string(),
had_newline: true,
file: 0,
})
.collect()
}
#[test]
fn doubling_loop_refuses_live_budget() {
let prog = parse_script(":a;N;100~100{h;G};ba");
let limits = ExecutionLimits::new().max_live_intermediate_bytes(1 << 18);
let budget = ExecutionBudget::new(&limits, Arc::new(AtomicBool::new(false)));
let mut machine = match test_machine(&prog, Some(budget)) {
Ok(machine) => machine,
Err(error) => panic!("live budget must construct: {error:?}"), };
let result = machine.run_segment(&many_x_lines(1500));
assert!(result.is_err(), "doubling loop must refuse, got {result:?}"); }
#[test]
fn commands_refuse_work_budget() {
let prog = parse_script(&"p;".repeat(20));
let limits = ExecutionLimits::new().max_work_units(10);
let budget = ExecutionBudget::new(&limits, Arc::new(AtomicBool::new(false)));
let mut machine = match test_machine(&prog, Some(budget)) {
Ok(machine) => machine,
Err(error) => panic!("live budget must construct: {error:?}"), };
let result = machine.run_segment(&test_lines());
assert!(result.is_err(), "work budget must refuse, got {result:?}"); }
#[test]
fn swap_fits_under_cap() {
let prog = parse_script("h;x");
let limits = ExecutionLimits::new().max_live_intermediate_bytes(64);
let budget = ExecutionBudget::new(&limits, Arc::new(AtomicBool::new(false)));
let mut machine = match test_machine(&prog, Some(budget)) {
Ok(machine) => machine,
Err(error) => panic!("live budget must construct: {error:?}"), };
let result = machine.run_segment(&many_x_lines(2));
assert!(result.is_ok(), "swap must fit, got {result:?}"); }
#[test]
fn delete_first_line_fits_under_cap() {
let prog = parse_script("D");
let limits = ExecutionLimits::new().max_live_intermediate_bytes(64);
let budget = ExecutionBudget::new(&limits, Arc::new(AtomicBool::new(false)));
let mut machine = match test_machine(&prog, Some(budget)) {
Ok(machine) => machine,
Err(error) => panic!("live budget must construct: {error:?}"), };
let result = machine.run_segment(&many_x_lines(2));
assert!(result.is_ok(), "drain must fit, got {result:?}"); }
#[test]
fn transliterate_same_size_fits() {
let prog = parse_script("y/abc/xyz/");
let limits = ExecutionLimits::new().max_live_intermediate_bytes(64);
let budget = ExecutionBudget::new(&limits, Arc::new(AtomicBool::new(false)));
let mut machine = match test_machine(&prog, Some(budget)) {
Ok(machine) => machine,
Err(error) => panic!("live budget must construct: {error:?}"), };
let result = machine.run_segment(&many_x_lines(2));
assert!(result.is_ok(), "same-size rewrite must fit, got {result:?}"); }
#[test]
fn widening_substitution_refuses() {
let prog = parse_script("s/x/xxxxxxxxxx/g");
let limits = ExecutionLimits::new().max_live_intermediate_bytes(64);
let budget = ExecutionBudget::new(&limits, Arc::new(AtomicBool::new(false)));
let mut machine = match test_machine(&prog, Some(budget)) {
Ok(machine) => machine,
Err(error) => panic!("live budget must construct: {error:?}"), };
let lines = vec![InputLine {
text: "xxxxxxxxxxxxxxxx".to_string(),
had_newline: true,
file: 0,
}];
let result = machine.run_segment(&lines);
assert!(
result.is_err(),
"widening rewrite must refuse, got {result:?}" );
}
#[test]
fn hold_append_refuses() {
let prog = parse_script("h;H;H;H;H;H;H;H;H;H;H");
let limits = ExecutionLimits::new().max_live_intermediate_bytes(64);
let budget = ExecutionBudget::new(&limits, Arc::new(AtomicBool::new(false)));
let mut machine = match test_machine(&prog, Some(budget)) {
Ok(machine) => machine,
Err(error) => panic!("live budget must construct: {error:?}"), };
let lines = vec![InputLine {
text: "xxxxxxxxxxxxxxxx".to_string(),
had_newline: true,
file: 0,
}];
let result = machine.run_segment(&lines);
assert!(result.is_err(), "hold appends must refuse, got {result:?}"); }
#[test]
fn reads_refuse_input_budget() {
let prog = parse_script("p");
let limits = ExecutionLimits::new().max_aggregate_input_bytes(5);
let budget = ExecutionBudget::new(&limits, Arc::new(AtomicBool::new(false)));
let mut machine = match test_machine(&prog, Some(budget)) {
Ok(machine) => machine,
Err(error) => panic!("live budget must construct: {error:?}"), };
let result = machine.run_segment(&many_x_lines(10));
assert!(result.is_err(), "input budget must refuse, got {result:?}"); }
}