use alloc::borrow::ToOwned;
use alloc::collections::BTreeMap;
use alloc::format;
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use brink_format::{
ClosureEnvEntry, ClosureValue, CountingFlags, DefinitionId, ListValue, LoadReport, OrderedMap,
SAVE_FORMAT_VERSION, SaveState, Value, VisitEntry,
};
use crate::StoryRng;
use crate::debug::NameResolver;
use crate::program::Program;
use crate::state::ContextAccess;
use crate::story::Story;
#[must_use]
pub fn save_state<C: ContextAccess + ?Sized>(program: &Program, ctx: &C) -> SaveState {
let resolver = NameResolver::new(program);
let globals: BTreeMap<String, Value> = (0..program.global_count())
.filter_map(|idx| {
program.global_slot_name(idx as usize).map(|name| {
let value = ctx.global(idx).clone();
crate::vm::note_value_share(&value);
(name.to_owned(), value)
})
})
.collect();
let global_ids: BTreeMap<String, DefinitionId> = (0..program.global_count())
.filter_map(|idx| {
let name = program.global_slot_name(idx as usize)?;
let id = program.global_id(idx as usize)?;
Some((name.to_owned(), id))
})
.collect();
let mut visits = Vec::new();
let mut turns = Vec::new();
for container in &program.containers {
if !container.counting_flags.contains(CountingFlags::VISITS) {
continue;
}
let id = container.id;
let count = ctx.visit_count(id);
if count > 0 {
visits.push(VisitEntry {
id,
path: resolver.def_path(id).map(str::to_owned),
count,
});
}
if let Some(turn) = ctx.turn_count(id) {
turns.push(VisitEntry {
id,
path: resolver.def_path(id).map(str::to_owned),
count: turn,
});
}
}
visits.sort_by_key(|e| e.id.to_raw());
turns.sort_by_key(|e| e.id.to_raw());
SaveState {
version: SAVE_FORMAT_VERSION,
globals,
global_ids,
visits,
turns,
turn_index: ctx.turn_index(),
rng_seed: ctx.rng_seed(),
previous_random: ctx.previous_random(),
suspended: None,
}
}
pub fn load_state<C: ContextAccess + ?Sized>(
program: &Program,
ctx: &mut C,
save: &SaveState,
) -> LoadReport {
let mut report = LoadReport::default();
let renames_matter = program.has_aliases();
for (name, value) in &save.globals {
match program.global_index(name) {
Some(idx) => {
let value = if renames_matter {
rebind_value(program, value, &mut report)
} else {
let cloned = value.clone();
crate::vm::note_value_share(&cloned);
cloned
};
ctx.set_global(idx, value);
}
None => {
if let Some(idx) = rebind_global_name(program, renames_matter, save, name) {
let value = rebind_value(program, value, &mut report);
ctx.set_global(idx, value);
} else {
if renames_matter && save.global_ids.contains_key(name) {
report
.unresolved_renames
.push(teach_was_message("global variable", name));
}
report.unknown_globals.push(name.clone());
}
}
}
}
ctx.set_turn_index(save.turn_index);
ctx.set_rng_seed(save.rng_seed);
ctx.set_previous_random(save.previous_random);
for e in &save.visits {
ctx.set_visit_count(rebind_address_key(program, e, &mut report), e.count);
}
for e in &save.turns {
ctx.set_turn_count(rebind_address_key(program, e, &mut report), e.count);
}
report
}
fn rebind_address_key(
program: &Program,
entry: &VisitEntry,
report: &mut LoadReport,
) -> DefinitionId {
let (id, unresolved) = rebind_address(program, entry.id);
if unresolved {
match &entry.path {
Some(path) if program.has_aliases() => {
report
.unresolved_renames
.push(teach_was_message("visit count", path));
}
Some(_) => {}
None => report.anonymous_states_dropped += 1,
}
}
id
}
fn rebind_global_name(
program: &Program,
renames_matter: bool,
save: &SaveState,
name: &str,
) -> Option<u32> {
if !renames_matter {
return None;
}
let old_id = *save.global_ids.get(name)?;
let new_id = program.resolve_alias(old_id)?;
program.resolve_global(new_id)
}
fn rebind_address(program: &Program, id: DefinitionId) -> (DefinitionId, bool) {
if program.knows_address(id) {
return (id, false);
}
match program.resolve_alias(id) {
Some(new_id) => (new_id, !program.knows_address(new_id)),
None => (id, true),
}
}
fn rebind_global(program: &Program, id: DefinitionId) -> (DefinitionId, bool) {
if program.knows_global(id) {
return (id, false);
}
match program.resolve_alias(id) {
Some(new_id) => (new_id, !program.knows_global(new_id)),
None => (id, true),
}
}
fn rebind_list_item(program: &Program, id: DefinitionId) -> (DefinitionId, bool) {
if program.knows_list_item(id) {
return (id, false);
}
match program.resolve_alias(id) {
Some(new_id) => (new_id, !program.knows_list_item(new_id)),
None => (id, true),
}
}
fn rebind_list_def(program: &Program, id: DefinitionId) -> (DefinitionId, bool) {
if program.knows_list_def(id) {
return (id, false);
}
match program.resolve_alias(id) {
Some(new_id) => (new_id, !program.knows_list_def(new_id)),
None => (id, true),
}
}
fn teach_was_message(subject: &str, path: &str) -> String {
let suggestion = path.split('.').next().unwrap_or(path);
format!(
"saved {subject} `{path}` resolves to nothing; if `{suggestion}` was renamed, add `#@was({suggestion})`"
)
}
fn teach_was_message_for_id(subject: &str, id: DefinitionId) -> String {
format!(
"saved {subject} {id} resolves to nothing; if its knot, stitch, or function was renamed, add `#@was(old_name)` to it"
)
}
fn rebind_value_address_id(
program: &Program,
subject: &str,
id: DefinitionId,
report: &mut LoadReport,
) -> DefinitionId {
let (new_id, unresolved) = rebind_address(program, id);
if unresolved {
report
.unresolved_renames
.push(teach_was_message_for_id(subject, id));
}
new_id
}
fn rebind_value_global_id(
program: &Program,
id: DefinitionId,
report: &mut LoadReport,
) -> DefinitionId {
let (new_id, unresolved) = rebind_global(program, id);
if unresolved {
report
.unresolved_renames
.push(teach_was_message_for_id("variable pointer", id));
}
new_id
}
fn rebind_value_list_item_id(
program: &Program,
id: DefinitionId,
report: &mut LoadReport,
) -> DefinitionId {
let (new_id, unresolved) = rebind_list_item(program, id);
if unresolved {
report
.unresolved_renames
.push(teach_was_message_for_id("list item", id));
}
new_id
}
fn rebind_value_list_def_id(
program: &Program,
id: DefinitionId,
report: &mut LoadReport,
) -> DefinitionId {
let (new_id, unresolved) = rebind_list_def(program, id);
if unresolved {
report
.unresolved_renames
.push(teach_was_message_for_id("list definition", id));
}
new_id
}
fn rebind_value(program: &Program, value: &Value, report: &mut LoadReport) -> Value {
match value {
Value::DivertTarget(id) => Value::DivertTarget(rebind_value_address_id(
program,
"divert target",
*id,
report,
)),
Value::FnRef(id) => Value::FnRef(rebind_value_address_id(program, "fn token", *id, report)),
Value::VariablePointer(id) => {
Value::VariablePointer(rebind_value_global_id(program, *id, report))
}
Value::List(list) => Value::List(Arc::new(ListValue {
items: list
.items
.iter()
.map(|id| rebind_value_list_item_id(program, *id, report))
.collect(),
origins: list
.origins
.iter()
.map(|id| rebind_value_list_def_id(program, *id, report))
.collect(),
})),
Value::Closure(c) => {
let target = rebind_value_address_id(program, "fn token", c.target, report);
let env = c
.env
.iter()
.map(|e| ClosureEnvEntry {
name: e.name,
is_ref: e.is_ref,
payload: rebind_value(program, &e.payload, report),
})
.collect();
Value::Closure(Arc::new(ClosureValue { target, env }))
}
Value::Array(items) => Value::array(
items
.iter()
.map(|v| rebind_value(program, v, report))
.collect::<Vec<_>>(),
),
Value::Map(m) => {
let rebound: OrderedMap = m
.iter()
.map(|(k, v)| (k.clone(), rebind_value(program, v, report)))
.collect();
Value::map(rebound)
}
Value::Record { shape, fields } => Value::Record {
shape: *shape,
fields: Arc::new(
fields
.iter()
.map(|v| rebind_value(program, v, report))
.collect(),
),
},
Value::Projection(p) => {
let cell = rebind_value_global_id(program, p.cell, report);
let segments = p
.segments
.iter()
.map(|seg| match seg {
brink_format::ProjSegment::Index(n) => brink_format::ProjSegment::Index(*n),
brink_format::ProjSegment::Key(v) => {
brink_format::ProjSegment::Key(rebind_value(program, v, report))
}
})
.collect();
Value::projection(cell, segments)
}
other => other.clone(),
}
}
impl<R: StoryRng> Story<R> {
#[must_use]
pub fn save_state(&self) -> SaveState {
save_state(self.program(), &self.default_context)
}
pub fn load_state(&mut self, save: &SaveState) -> LoadReport {
let program = self.program_arc();
load_state(&program, &mut self.default_context, save)
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use crate::link;
use crate::rng::FastRng;
fn compile_for_flow(src: &str) -> (Program, Vec<Vec<brink_format::LineEntry>>) {
let out = brink_compiler::compile("t.ink", |p| {
if p == "t.ink" {
Ok(src.to_string())
} else {
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"no such include",
))
}
})
.expect("compile");
link(&out.data).expect("link")
}
#[test]
fn visits_are_sorted_by_id_regardless_of_visit_order() {
let (program, tables) = compile_for_flow(
"-> alpha\n\
=== alpha ===\n\
Alpha.\n\
-> DONE\n\
=== beta ===\n\
Beta.\n\
-> DONE\n\
=== gamma ===\n\
Gamma.\n\
-> DONE\n\
=== reader ===\n\
{READ_COUNT(-> alpha)} {READ_COUNT(-> beta)} {READ_COUNT(-> gamma)}\n\
-> DONE\n",
);
let program = Arc::new(program);
let mut story = crate::Story::<FastRng>::new(Arc::clone(&program), tables);
story.continue_maximally().expect("continue");
story.choose_path_string("gamma").expect("jump");
story.continue_maximally().expect("continue");
story.choose_path_string("beta").expect("jump");
story.continue_maximally().expect("continue");
let save = story.save_state();
assert_eq!(
save.visits.len(),
3,
"alpha/beta/gamma should each have a visit entry: {:?}",
save.visits
);
let ids: Vec<u64> = save.visits.iter().map(|e| e.id.to_raw()).collect();
let mut sorted = ids.clone();
sorted.sort_unstable();
assert_eq!(ids, sorted, "SaveState::visits must be sorted by id");
}
#[test]
fn anonymous_unresolved_visit_entry_is_counted_in_load_report() {
let (program, tables) = compile_for_flow(
"-> alpha\n\
=== alpha ===\n\
Alpha.\n\
-> DONE\n",
);
let program = Arc::new(program);
let mut story = crate::Story::<FastRng>::new(Arc::clone(&program), tables);
story.continue_maximally().expect("continue");
let mut save = story.save_state();
assert!(
save.visits.iter().all(|e| e.path.is_some()),
"sanity: the real save has no anonymous entries to confuse this \
test: {:?}",
save.visits
);
let phantom_id =
brink_format::DefinitionId::new(brink_format::DefinitionTag::Address, u64::MAX);
save.visits.push(brink_format::VisitEntry {
id: phantom_id,
path: None,
count: 3,
});
let report = story.load_state(&save);
assert_eq!(report.anonymous_states_dropped, 1, "{report:?}");
assert!(!report.is_clean(), "{report:?}");
assert!(
report.unresolved_renames.is_empty(),
"an anonymous miss has no path to teach a #@was fix against, \
so it must never land in unresolved_renames: {report:?}"
);
}
#[test]
fn named_unresolved_visit_entry_is_not_counted_as_anonymous() {
let (program, tables) = compile_for_flow(
"-> alpha\n\
=== alpha ===\n\
Alpha.\n\
-> DONE\n",
);
let program = Arc::new(program);
let mut story = crate::Story::<FastRng>::new(Arc::clone(&program), tables);
let phantom_id =
brink_format::DefinitionId::new(brink_format::DefinitionTag::Address, u64::MAX);
let mut save = story.save_state();
save.visits.push(brink_format::VisitEntry {
id: phantom_id,
path: Some("forest.gone_knot".to_owned()),
count: 3,
});
let report = story.load_state(&save);
assert_eq!(
report.anonymous_states_dropped, 0,
"a named miss is not an anonymous drop: {report:?}"
);
assert!(
report.unresolved_renames.is_empty(),
"no #@was alias table on this program, so the named miss stays \
silent exactly like before M-3: {report:?}"
);
}
#[test]
fn a_real_content_edit_shifting_an_anonymous_choice_is_counted_in_load_report() {
let (program_a, tables_a) = compile_for_flow(
"-> alpha\n\
=== alpha ===\n\
* [pick]\n\
\tPicked.\n\
\t-> DONE\n",
);
let program_a = Arc::new(program_a);
let mut story_a = crate::Story::<FastRng>::new(Arc::clone(&program_a), tables_a);
story_a.continue_maximally().expect("continue");
story_a.choose(0).expect("choose `pick`");
story_a.continue_maximally().expect("continue");
let save = story_a.save_state();
assert_eq!(
save.visits.len(),
1,
"sanity: exactly `pick`'s own anonymous visit entry: {:?}",
save.visits
);
assert!(
save.visits[0].path.is_none(),
"sanity: a real unlabeled once-only choice's container really \
does save with `path: None`: {:?}",
save.visits[0]
);
let (program_b, tables_b) = compile_for_flow(
"-> alpha\n\
=== alpha ===\n\
* (extra) [extra]\n\
\tExtra.\n\
\t-> DONE\n\
* [pick]\n\
\tPicked.\n\
\t-> DONE\n",
);
let program_b = Arc::new(program_b);
let mut story_b = crate::Story::<FastRng>::new(Arc::clone(&program_b), tables_b);
let report = story_b.load_state(&save);
assert_eq!(
report.anonymous_states_dropped, 2,
"the shifted choice's visit AND turn entry both go unresolved: {report:?}"
);
assert!(!report.is_clean(), "{report:?}");
assert!(
report.unresolved_renames.is_empty(),
"an anonymous miss has no path to teach a #@was fix against: {report:?}"
);
}
}