Skip to main content

bynk_lsp/
agent_state_request.rs

1//! #2196: `bynk/agentStateModel` — the agent state view's custom LSP request
2//! (agent-state-views track, slice 2).
3//!
4//! Agent-scoped, not handler-scoped: the cursor anywhere inside an `agent`
5//! declaration (its header, a `store` field, a handler body) selects that
6//! agent, and the response is the whole agent's state model
7//! ([`bynk_ide::agent_state`]). Served from the committed round by
8//! re-parsing the snapshot, like `bynk/sequenceModel` (ADR 0301's posture); no
9//! refresh nudge, since the client re-pulls (ADR 0504).
10//!
11//! Also here: the sites of the per-agent `Show State` lens, and the wire
12//! shape — a serde mirror of the model with `Span`s lowered to LSP `Range`s,
13//! plus each handler's name and range (the table's rows, and what the client
14//! highlights for the handler under the cursor).
15
16use std::collections::{HashMap, HashSet};
17use std::sync::Arc;
18
19use bynk_ide::agent_effects::{self, HandlerEffects};
20use bynk_ide::agent_state::{self, AgentStateModel, Certainty, Target};
21use bynk_syntax::ast::{AgentDecl, CommonsItem, SourceUnit, TypeDecl};
22use bynk_syntax::span::Span;
23use tower_lsp::lsp_types::Range;
24
25/// The `bynk/agentStateModel` request payload. `rename_all = "camelCase"` is
26/// load-bearing — the client sends `textDocument`/`position` (see
27/// `sequence_request::SequenceModelParams`).
28#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
29#[serde(rename_all = "camelCase")]
30pub struct AgentStateModelParams {
31    pub text_document: tower_lsp::lsp_types::TextDocumentIdentifier,
32    pub position: tower_lsp::lsp_types::Position,
33}
34
35/// The agents of `text`, re-parsed. Empty for a commons, a suite, or a file
36/// that doesn't tokenize.
37fn agents(text: &str) -> Vec<AgentDecl> {
38    let Ok(tokens) = bynk_syntax::lexer::tokenize(text) else {
39        return Vec::new();
40    };
41    let (unit, _errs) = bynk_syntax::parser::parse_unit_with_recovery(&tokens, text);
42    let items = match unit {
43        Some(SourceUnit::Context(c)) => c.items,
44        Some(SourceUnit::Adapter(a)) => a.items,
45        _ => return Vec::new(),
46    };
47    items
48        .into_iter()
49        .filter_map(|i| match i {
50            CommonsItem::Agent(a) => Some(a),
51            _ => None,
52        })
53        .collect()
54}
55
56/// The agent enclosing `offset`, with its model built against `types` (the
57/// unit's combined type table). `None` outside every agent.
58pub fn agent_state_at(
59    text: &str,
60    offset: usize,
61    types: &HashMap<String, Arc<TypeDecl>>,
62) -> Option<(AgentDecl, AgentStateModel)> {
63    let agent = agents(text)
64        .into_iter()
65        .find(|a| a.span.start <= offset && offset < a.span.end)?;
66    let model = agent_state::agent_state_model(&agent, types);
67    Some((agent, model))
68}
69
70/// Every agent's name, for the per-agent `Show State` lens.
71pub fn agent_lens_sites(text: &str) -> Vec<Span> {
72    agents(text).iter().map(|a| a.name.span).collect()
73}
74
75#[derive(Debug, Clone, serde::Serialize)]
76pub struct WireAgentStateModel {
77    pub agent: String,
78    pub range: Range,
79    pub handlers: Vec<WireHandler>,
80    pub machines: Vec<WireStateMachine>,
81    /// #2198: what each handler writes, reads and calls, path by path.
82    pub effects: Vec<WireHandlerEffects>,
83}
84
85#[derive(Debug, Clone, serde::Serialize)]
86pub struct WireHandlerEffects {
87    pub handler: String,
88    pub rows: Vec<WireEffectRow>,
89    pub truncated: bool,
90}
91
92#[derive(Debug, Clone, serde::Serialize)]
93pub struct WireEffectRow {
94    pub guards: Vec<String>,
95    pub writes: Vec<WireEffectWrite>,
96    pub reads: Vec<String>,
97    pub calls: Vec<String>,
98    pub outcomes: Vec<String>,
99}
100
101#[derive(Debug, Clone, serde::Serialize)]
102pub struct WireEffectWrite {
103    pub field: String,
104    pub op: String,
105    pub key: Option<String>,
106    pub range: Range,
107}
108
109/// #2198: the effects of `agent`'s handlers, with `agents` naming the
110/// agents a handler can call (the context's agent table).
111pub fn effects_of(agent: &AgentDecl, agents: &HashSet<String>) -> Vec<HandlerEffects> {
112    agent_effects::agent_effects(agent, agents)
113}
114
115#[derive(Debug, Clone, serde::Serialize)]
116pub struct WireHandler {
117    pub name: String,
118    pub range: Range,
119}
120
121#[derive(Debug, Clone, serde::Serialize)]
122#[serde(rename_all = "camelCase")]
123pub struct WireStateMachine {
124    pub field: String,
125    pub field_range: Range,
126    pub states: Vec<String>,
127    pub initial: Option<String>,
128    pub edges: Vec<WireEdge>,
129    pub reachable: Vec<String>,
130    /// #2219: the states the agent can be in after a commit — the only start
131    /// states the overlay checks, since the genesis commit runs no
132    /// `transition`.
133    pub reachable_after_commit: Vec<String>,
134    pub truncated: Vec<String>,
135    /// #2197: `transition`s on the field the overlay could not evaluate.
136    pub not_evaluated: Vec<WireNamed>,
137}
138
139#[derive(Debug, Clone, serde::Serialize)]
140pub struct WireNamed {
141    pub name: String,
142    pub range: Range,
143}
144
145/// #2197: a `transition` this edge can violate at commit.
146#[derive(Debug, Clone, serde::Serialize)]
147pub struct WireViolation {
148    pub transition: String,
149    pub range: Range,
150    /// `"Known"` or `"Possible"`.
151    pub certainty: &'static str,
152    pub from: Vec<String>,
153    pub targets: Vec<String>,
154}
155
156#[derive(Debug, Clone, serde::Serialize)]
157pub struct WireEdge {
158    pub handler: String,
159    pub from: Vec<String>,
160    pub guards: Vec<String>,
161    /// `{ "kind": "State", "state": "Placed" }`, `{ "kind": "Unknown" }` or
162    /// `{ "kind": "Unchanged" }`.
163    pub to: WireTarget,
164    pub outcome: Option<String>,
165    pub range: Range,
166    pub violations: Vec<WireViolation>,
167}
168
169#[derive(Debug, Clone, serde::Serialize)]
170#[serde(tag = "kind")]
171pub enum WireTarget {
172    State { state: String },
173    Unknown,
174    Unchanged,
175}
176
177pub fn to_wire(
178    agent: &AgentDecl,
179    model: &AgentStateModel,
180    effects: &[HandlerEffects],
181    text: &str,
182) -> WireAgentStateModel {
183    let range = |s: Span| crate::position::span_to_range(text, s);
184    WireAgentStateModel {
185        effects: effects
186            .iter()
187            .map(|h| WireHandlerEffects {
188                handler: h.handler.clone(),
189                truncated: h.truncated,
190                rows: h
191                    .rows
192                    .iter()
193                    .map(|r| WireEffectRow {
194                        guards: r.guards.clone(),
195                        writes: r
196                            .writes
197                            .iter()
198                            .map(|w| WireEffectWrite {
199                                field: w.field.clone(),
200                                op: w.op.clone(),
201                                key: w.key.clone(),
202                                range: range(w.span),
203                            })
204                            .collect(),
205                        reads: r.reads.clone(),
206                        calls: r.calls.clone(),
207                        outcomes: r.outcomes.clone(),
208                    })
209                    .collect(),
210            })
211            .collect(),
212        agent: model.agent.clone(),
213        range: range(agent.name.span),
214        handlers: agent
215            .handlers
216            .iter()
217            .map(|h| WireHandler {
218                name: h
219                    .method_name
220                    .as_ref()
221                    .map_or_else(|| "call".to_string(), |m| m.name.clone()),
222                range: range(h.method_name.as_ref().map_or(h.span, |m| m.span)),
223            })
224            .collect(),
225        machines: model
226            .machines
227            .iter()
228            .map(|m| WireStateMachine {
229                field: m.field.clone(),
230                field_range: range(m.field_span),
231                states: m.states.clone(),
232                initial: m.initial.clone(),
233                edges: m
234                    .edges
235                    .iter()
236                    .map(|e| WireEdge {
237                        handler: e.handler.clone(),
238                        from: e.from.clone(),
239                        guards: e.guards.clone(),
240                        to: match &e.to {
241                            Target::State(s) => WireTarget::State { state: s.clone() },
242                            Target::Unknown => WireTarget::Unknown,
243                            Target::Unchanged => WireTarget::Unchanged,
244                        },
245                        outcome: e.outcome.clone(),
246                        range: range(e.span),
247                        violations: e
248                            .violations
249                            .iter()
250                            .map(|v| WireViolation {
251                                transition: v.transition.clone(),
252                                range: range(v.span),
253                                certainty: match v.certainty {
254                                    Certainty::Known => "Known",
255                                    Certainty::Possible => "Possible",
256                                },
257                                from: v.from.clone(),
258                                targets: v.targets.clone(),
259                            })
260                            .collect(),
261                    })
262                    .collect(),
263                reachable: m.reachable.clone(),
264                reachable_after_commit: m.reachable_after_commit.clone(),
265                truncated: m.truncated.clone(),
266                not_evaluated: m
267                    .not_evaluated
268                    .iter()
269                    .map(|n| WireNamed {
270                        name: n.transition.clone(),
271                        range: range(n.span),
272                    })
273                    .collect(),
274            })
275            .collect(),
276    }
277}