Skip to main content

sim_codec_bridge/
move_book.rs

1use std::collections::BTreeMap;
2
3use sim_kernel::{Error, Result, Symbol};
4
5/// Parent-intent rule for a BRIDGE move.
6#[derive(Clone, Debug, PartialEq, Eq)]
7pub enum ReplyRule {
8    /// The move opens a new thread and may not cite parents.
9    Opens,
10    /// The move may answer any parent intent.
11    Any,
12    /// The move may answer any parent intent except `receipt`.
13    AnyNonReceipt,
14    /// The move may answer only the listed parent intents.
15    Only(Vec<Symbol>),
16}
17
18/// Registered dialogue move specification.
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct BridgeMoveSpec {
21    /// Move intent symbol.
22    pub intent: Symbol,
23    /// Parent intent legality.
24    pub replies_to: ReplyRule,
25    /// Required part kinds.
26    pub requires_parts: Vec<Symbol>,
27    /// Alternative part-kind groups; every group must have at least one member present.
28    pub requires_any_parts: Vec<Vec<Symbol>>,
29    /// Whether this move is terminal.
30    pub terminal: bool,
31}
32
33/// Registry of legal BRIDGE moves.
34#[derive(Clone, Debug, Default, PartialEq, Eq)]
35pub struct BridgeMoveBook {
36    moves: BTreeMap<Symbol, BridgeMoveSpec>,
37}
38
39impl BridgeMoveBook {
40    /// Builds an empty move book.
41    pub fn new() -> Self {
42        Self::default()
43    }
44
45    /// Registers a move spec.
46    pub fn register(&mut self, spec: BridgeMoveSpec) {
47        self.moves.insert(spec.intent.clone(), spec);
48    }
49
50    /// Returns the registered spec for `intent`.
51    pub fn spec(&self, intent: &Symbol) -> Option<&BridgeMoveSpec> {
52        self.moves.get(intent)
53    }
54
55    /// Returns all registered move specs.
56    pub fn specs(&self) -> impl Iterator<Item = &BridgeMoveSpec> {
57        self.moves.values()
58    }
59
60    /// Returns registered intents that may reply to `parents`.
61    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    /// Checks dialogue legality for a move, its parent intents, and its part kinds.
70    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
96/// Builds the standard BRIDGE move book.
97pub 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}