use crate::codegen::Slot;
use crate::lex;
use crate::proc::StdinEvent;
#[derive(Clone)]
enum LogEntry {
AddedFile {
stdin_events: usize,
},
AddedStmt {
opened_scope: bool,
stdin_events: usize,
},
ReplacedFile {
index: usize,
previous: String,
stdin_events: usize,
},
}
#[derive(Default, Clone)]
pub struct Session {
pub file_items: Vec<String>,
pub stmts: Vec<String>,
stmt_stdin_events: Vec<usize>,
stdin_tape: Vec<StdinEvent>,
pub(crate) scope_depth: usize,
pub counter: usize,
inputs: Vec<String>,
external_replay_warning_shown: bool,
log: Vec<LogEntry>,
}
impl Session {
pub fn commit(&mut self, text: &str, slot: Slot) {
let stored = match slot {
Slot::FileScope => {
self.file_items.push(text.to_string());
self.log.push(LogEntry::AddedFile { stdin_events: 0 });
return;
}
Slot::Stmt => text.to_string(),
Slot::Expr if lex::may_have_side_effects(text) => {
format!("(void)({text});")
}
Slot::Expr => return,
};
self.stmts.push(indent(&stored));
self.stmt_stdin_events.push(0);
self.log.push(LogEntry::AddedStmt {
opened_scope: false,
stdin_events: 0,
});
}
pub fn commit_scoped(&mut self, text: &str) {
self.stmts.push(indent("{"));
self.stmt_stdin_events.push(0);
self.stmts.push(indent(text));
self.stmt_stdin_events.push(0);
self.scope_depth += 1;
self.log.push(LogEntry::AddedStmt {
opened_scope: true,
stdin_events: 0,
});
}
pub fn replace_file(&mut self, index: usize, text: &str) {
let previous = std::mem::replace(&mut self.file_items[index], text.to_string());
self.log.push(LogEntry::ReplacedFile {
index,
previous,
stdin_events: 0,
});
}
pub fn attach_stdin_events(&mut self, events: Vec<StdinEvent>) {
if events.is_empty() {
return;
}
let count = events.len();
match self.log.last_mut().expect("stdin input must change state") {
LogEntry::AddedStmt { stdin_events, .. } => {
*stdin_events += count;
*self
.stmt_stdin_events
.last_mut()
.expect("statement stdin annotation") += count;
}
LogEntry::AddedFile { stdin_events } | LogEntry::ReplacedFile { stdin_events, .. } => {
*stdin_events += count;
}
}
self.stdin_tape.extend(events);
}
pub fn stdin_tape(&self) -> &[StdinEvent] {
&self.stdin_tape
}
pub fn stmt_stdin_event_count(&self, index: usize) -> usize {
self.stmt_stdin_events.get(index).copied().unwrap_or(0)
}
pub fn undo(&mut self) -> Option<String> {
let (removed, stdin_events) = match self.log.pop()? {
LogEntry::AddedFile { stdin_events } => (self.file_items.pop(), stdin_events),
LogEntry::AddedStmt {
opened_scope,
stdin_events,
} => {
let removed = self.stmts.pop().map(|s| s.trim().to_string());
self.stmt_stdin_events.pop();
if opened_scope {
let brace = self.stmts.pop();
self.stmt_stdin_events.pop();
debug_assert_eq!(brace.as_deref().map(str::trim), Some("{"));
self.scope_depth = self.scope_depth.saturating_sub(1);
}
(removed, stdin_events)
}
LogEntry::ReplacedFile {
index,
previous,
stdin_events,
} => {
let removed = std::mem::replace(&mut self.file_items[index], previous);
(Some(removed), stdin_events)
}
};
self.stdin_tape
.truncate(self.stdin_tape.len().saturating_sub(stdin_events));
removed
}
pub fn remember_input(&mut self, number: usize, text: &str) {
debug_assert_eq!(number, self.inputs.len() + 1);
self.inputs.push(text.to_string());
}
pub fn input(&self, number: usize) -> Option<&str> {
number
.checked_sub(1)
.and_then(|index| self.inputs.get(index).map(String::as_str))
}
pub fn last_input(&self) -> Option<&str> {
self.inputs.last().map(String::as_str)
}
pub fn external_replay_warning_shown(&self) -> bool {
self.external_replay_warning_shown
}
pub fn mark_external_replay_warning_shown(&mut self) {
self.external_replay_warning_shown = true;
}
pub fn identifiers(&self) -> Vec<String> {
let mut seen = std::collections::BTreeSet::new();
for text in self.file_items.iter().chain(self.stmts.iter()) {
for w in lex::identifiers(text) {
if w.len() >= 2 {
seen.insert(w);
}
}
}
seen.into_iter().collect()
}
pub fn reset(&mut self) {
self.file_items.clear();
self.stmts.clear();
self.stmt_stdin_events.clear();
self.stdin_tape.clear();
self.scope_depth = 0;
self.log.clear();
self.counter = 0;
self.inputs.clear();
self.external_replay_warning_shown = false;
}
}
fn indent(s: &str) -> String {
s.lines()
.map(|l| format!(" {l}"))
.collect::<Vec<_>>()
.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn numbered_inputs_are_available_until_reset() {
let mut session = Session::default();
session.remember_input(1, "int broken = ;");
session.remember_input(2, "40 + 2");
assert_eq!(session.input(1), Some("int broken = ;"));
assert_eq!(session.input(2), Some("40 + 2"));
assert_eq!(session.input(0), None);
assert_eq!(session.input(3), None);
assert_eq!(session.last_input(), Some("40 + 2"));
session.reset();
assert_eq!(session.input(1), None);
}
#[test]
fn undo_closes_a_shadowing_scope() {
let mut session = Session::default();
session.commit("int x = 1;", Slot::Stmt);
session.commit_scoped("int x = 2;");
session.attach_stdin_events(vec![StdinEvent {
bytes: b"2\n".to_vec(),
eof: false,
}]);
assert_eq!(session.stdin_tape().len(), 1);
assert_eq!(session.stmt_stdin_event_count(2), 1);
assert_eq!(session.scope_depth, 1);
assert_eq!(session.undo().as_deref(), Some("int x = 2;"));
assert_eq!(session.scope_depth, 0);
assert_eq!(session.stmts.len(), 1);
assert!(session.stdin_tape().is_empty());
}
#[test]
fn undo_restores_a_replaced_file_item() {
let mut session = Session::default();
session.commit("int f(void) { return 1; }", Slot::FileScope);
session.replace_file(0, "int f(void) { return 2; }");
assert_eq!(session.undo().as_deref(), Some("int f(void) { return 2; }"));
assert_eq!(session.file_items[0], "int f(void) { return 1; }");
}
}