1use std::collections::BTreeMap;
2
3use sim_kernel::{Error, Result, Symbol};
4
5#[derive(Clone, Debug, PartialEq, Eq)]
7pub enum ReplyRule {
8 Opens,
10 Any,
12 AnyNonReceipt,
14 Only(Vec<Symbol>),
16}
17
18#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct BridgeMoveSpec {
21 pub intent: Symbol,
23 pub replies_to: ReplyRule,
25 pub requires_parts: Vec<Symbol>,
27 pub requires_any_parts: Vec<Vec<Symbol>>,
29 pub terminal: bool,
31}
32
33#[derive(Clone, Debug, Default, PartialEq, Eq)]
35pub struct BridgeMoveBook {
36 moves: BTreeMap<Symbol, BridgeMoveSpec>,
37}
38
39impl BridgeMoveBook {
40 pub fn new() -> Self {
42 Self::default()
43 }
44
45 pub fn register(&mut self, spec: BridgeMoveSpec) {
47 self.moves.insert(spec.intent.clone(), spec);
48 }
49
50 pub fn spec(&self, intent: &Symbol) -> Option<&BridgeMoveSpec> {
52 self.moves.get(intent)
53 }
54
55 pub fn specs(&self) -> impl Iterator<Item = &BridgeMoveSpec> {
57 self.moves.values()
58 }
59
60 pub fn legal_reply_intents(&self, parents: &[Symbol]) -> Vec<Symbol> {
62 self.moves
63 .values()
64 .filter(|spec| check_reply_rule(&spec.intent, &spec.replies_to, parents).is_ok())
65 .map(|spec| spec.intent.clone())
66 .collect()
67 }
68
69 pub fn check_move(&self, intent: &Symbol, parents: &[Symbol], parts: &[Symbol]) -> Result<()> {
71 let spec = self
72 .moves
73 .get(intent)
74 .ok_or_else(|| Error::Eval(format!("unknown BRIDGE move {intent}")))?;
75 check_reply_rule(intent, &spec.replies_to, parents)?;
76 for required in &spec.requires_parts {
77 if !parts.contains(required) {
78 return Err(Error::Eval(format!("{intent} requires a {required} part")));
79 }
80 }
81 for alternatives in &spec.requires_any_parts {
82 if alternatives.iter().any(|required| parts.contains(required)) {
83 continue;
84 }
85 let names = alternatives
86 .iter()
87 .map(Symbol::as_qualified_str)
88 .collect::<Vec<_>>()
89 .join(" or ");
90 return Err(Error::Eval(format!("{intent} requires {names}")));
91 }
92 Ok(())
93 }
94}
95
96pub fn standard_move_book() -> BridgeMoveBook {
98 let mut book = BridgeMoveBook::new();
99 for spec in [
100 move_spec_with_any(
101 "request",
102 ReplyRule::Opens,
103 &["Return"],
104 &[&["Frame", "Call", "Weave"]],
105 false,
106 ),
107 move_spec(
108 "offer",
109 ReplyRule::Only(vec![intent("request")]),
110 &["Return"],
111 false,
112 ),
113 move_spec(
114 "reply",
115 ReplyRule::Only(vec![intent("request")]),
116 &["Return"],
117 false,
118 ),
119 move_spec(
120 "review",
121 ReplyRule::Only(vec![intent("offer"), intent("reply"), intent("patch")]),
122 &["Review"],
123 false,
124 ),
125 move_spec(
126 "vote",
127 ReplyRule::Only(vec![intent("offer"), intent("reply")]),
128 &["Vote"],
129 false,
130 ),
131 move_spec(
132 "patch",
133 ReplyRule::Only(vec![intent("receipt"), intent("review"), intent("reply")]),
134 &["Patch"],
135 false,
136 ),
137 move_spec("fetch", ReplyRule::AnyNonReceipt, &["Fetch"], false),
138 move_spec("receipt", ReplyRule::AnyNonReceipt, &["Receipt"], true),
139 move_spec(
140 "attest",
141 ReplyRule::Only(vec![intent("reply"), intent("receipt")]),
142 &["Attest"],
143 true,
144 ),
145 move_spec("error", ReplyRule::Any, &["Receipt"], true),
146 ] {
147 book.register(spec);
148 }
149 book
150}
151
152fn check_reply_rule(current_intent: &Symbol, rule: &ReplyRule, parents: &[Symbol]) -> Result<()> {
153 match rule {
154 ReplyRule::Opens => {
155 if parents.is_empty() {
156 Ok(())
157 } else {
158 Err(Error::Eval(format!(
159 "{current_intent} opens a thread; it cannot reply"
160 )))
161 }
162 }
163 ReplyRule::Any => require_parent(current_intent, parents),
164 ReplyRule::AnyNonReceipt => {
165 require_parent(current_intent, parents)?;
166 for parent in parents {
167 if parent == &intent("receipt") {
168 return Err(Error::Eval(format!(
169 "{current_intent} may not answer {parent}"
170 )));
171 }
172 }
173 Ok(())
174 }
175 ReplyRule::Only(allowed) => {
176 require_parent(current_intent, parents)?;
177 for parent in parents {
178 if !allowed.contains(parent) {
179 return Err(Error::Eval(format!(
180 "{current_intent} may not answer {parent}"
181 )));
182 }
183 }
184 Ok(())
185 }
186 }
187}
188
189fn require_parent(intent: &Symbol, parents: &[Symbol]) -> Result<()> {
190 if parents.is_empty() {
191 Err(Error::Eval(format!("{intent} requires a parent move")))
192 } else {
193 Ok(())
194 }
195}
196
197fn move_spec(
198 name: &str,
199 replies_to: ReplyRule,
200 required_parts: &[&str],
201 terminal: bool,
202) -> BridgeMoveSpec {
203 move_spec_with_any(name, replies_to, required_parts, &[], terminal)
204}
205
206fn move_spec_with_any(
207 name: &str,
208 replies_to: ReplyRule,
209 required_parts: &[&str],
210 required_any_parts: &[&[&str]],
211 terminal: bool,
212) -> BridgeMoveSpec {
213 BridgeMoveSpec {
214 intent: intent(name),
215 replies_to,
216 requires_parts: required_parts
217 .iter()
218 .map(|name| Symbol::qualified("bridge", *name))
219 .collect(),
220 requires_any_parts: required_any_parts
221 .iter()
222 .map(|group| {
223 group
224 .iter()
225 .map(|name| Symbol::qualified("bridge", *name))
226 .collect()
227 })
228 .collect(),
229 terminal,
230 }
231}
232
233fn intent(name: &str) -> Symbol {
234 Symbol::new(name)
235}