1#![allow(clippy::too_many_lines)]
26
27use std::collections::HashMap;
28use std::path::Path;
29
30use panproto_gat::{
31 FreeModelConfig, Theory, TheoryMorphism, VarContext, free_model, normalize, typecheck_term,
32};
33use panproto_theory_dsl::compile_theory::parse_term;
34use panproto_theory_dsl::{builtin_resolver, load_and_compile};
35
36#[derive(Default)]
39pub struct Repl {
40 pub theories: HashMap<String, Theory>,
42 pub morphisms: HashMap<String, TheoryMorphism>,
44 pub active: Option<String>,
46}
47
48#[derive(Debug, Clone)]
50pub enum ReplOutcome {
51 Output(String),
53 Error(String),
55 Quit,
57}
58
59impl Repl {
60 #[must_use]
62 pub fn new() -> Self {
63 Self::default()
64 }
65
66 #[must_use]
69 pub fn handle_line(&mut self, line: &str) -> ReplOutcome {
70 let trimmed = line.trim();
71 if trimmed.is_empty() {
72 return ReplOutcome::Output(String::new());
73 }
74 if let Some(rest) = trimmed.strip_prefix(':') {
75 self.handle_command(rest.trim())
76 } else {
77 self.handle_term_typecheck(trimmed)
78 }
79 }
80
81 fn handle_command(&mut self, rest: &str) -> ReplOutcome {
82 let (head, tail) = split_first_word(rest);
83 match head {
84 "quit" | "q" => ReplOutcome::Quit,
85 "load" => self.cmd_load(tail),
86 "theories" => self.cmd_theories(),
87 "use" => self.cmd_use(tail),
88 "sorts" => self.cmd_sorts(),
89 "ops" => self.cmd_ops(),
90 "type" => self.cmd_type(tail),
91 "normalize" => self.cmd_normalize(tail),
92 "model" => self.cmd_model(tail),
93 "instance" => self.cmd_instance(tail),
94 other => ReplOutcome::Error(format!("unknown command :{other}")),
95 }
96 }
97
98 fn cmd_load(&mut self, path: &str) -> ReplOutcome {
99 if path.is_empty() {
100 return ReplOutcome::Error("usage: :load <path>".to_string());
101 }
102 let p = Path::new(path);
103 let resolver = builtin_resolver();
104 match load_and_compile(p, &resolver) {
105 Ok(set) => {
106 let mut loaded: Vec<String> = Vec::new();
107 for (name, theory) in set.theories {
108 loaded.push(name.clone());
109 self.theories.insert(name, theory);
110 }
111 for (name, morph) in set.morphisms {
112 self.morphisms.insert(name, morph);
113 }
114 if self.active.is_none() {
115 self.active = loaded.first().cloned();
116 }
117 let msg = if loaded.is_empty() {
118 "loaded no theories".to_string()
119 } else {
120 format!("loaded: {}", loaded.join(", "))
121 };
122 ReplOutcome::Output(msg)
123 }
124 Err(e) => ReplOutcome::Error(format!("load failed: {e}")),
125 }
126 }
127
128 fn cmd_theories(&self) -> ReplOutcome {
129 if self.theories.is_empty() {
130 return ReplOutcome::Output("(no theories loaded)".to_string());
131 }
132 let mut names: Vec<&String> = self.theories.keys().collect();
133 names.sort();
134 let marked: Vec<String> = names
135 .into_iter()
136 .map(|n| {
137 if self.active.as_deref() == Some(n.as_str()) {
138 format!("* {n}")
139 } else {
140 format!(" {n}")
141 }
142 })
143 .collect();
144 ReplOutcome::Output(marked.join("\n"))
145 }
146
147 fn cmd_use(&mut self, name: &str) -> ReplOutcome {
148 if name.is_empty() {
149 return ReplOutcome::Error("usage: :use <theory>".to_string());
150 }
151 if !self.theories.contains_key(name) {
152 return ReplOutcome::Error(format!("theory '{name}' not loaded"));
153 }
154 self.active = Some(name.to_string());
155 ReplOutcome::Output(format!("active theory: {name}"))
156 }
157
158 fn active_theory(&self) -> Result<&Theory, ReplOutcome> {
159 let name = self.active.as_ref().ok_or_else(|| {
160 ReplOutcome::Error("no active theory; use :load and :use".to_string())
161 })?;
162 self.theories
163 .get(name)
164 .ok_or_else(|| ReplOutcome::Error(format!("active theory '{name}' missing")))
165 }
166
167 fn cmd_sorts(&self) -> ReplOutcome {
168 let theory = match self.active_theory() {
169 Ok(t) => t,
170 Err(e) => return e,
171 };
172 if theory.sorts.is_empty() {
173 return ReplOutcome::Output("(no sorts)".to_string());
174 }
175 let lines: Vec<String> = theory
176 .sorts
177 .iter()
178 .map(|s| {
179 if s.params.is_empty() {
180 s.name.to_string()
181 } else {
182 let params: Vec<String> = s
183 .params
184 .iter()
185 .map(|p| format!("{}: {}", p.name, p.sort))
186 .collect();
187 format!("{}({})", s.name, params.join(", "))
188 }
189 })
190 .collect();
191 ReplOutcome::Output(lines.join("\n"))
192 }
193
194 fn cmd_ops(&self) -> ReplOutcome {
195 let theory = match self.active_theory() {
196 Ok(t) => t,
197 Err(e) => return e,
198 };
199 if theory.ops.is_empty() {
200 return ReplOutcome::Output("(no ops)".to_string());
201 }
202 let lines: Vec<String> = theory
203 .ops
204 .iter()
205 .map(|op| {
206 let inputs: Vec<String> = op
207 .inputs
208 .iter()
209 .map(|(n, s, _)| format!("{n}: {s}"))
210 .collect();
211 format!("{}({}) -> {}", op.name, inputs.join(", "), op.output)
212 })
213 .collect();
214 ReplOutcome::Output(lines.join("\n"))
215 }
216
217 fn cmd_type(&self, term_src: &str) -> ReplOutcome {
218 let theory = match self.active_theory() {
219 Ok(t) => t,
220 Err(e) => return e,
221 };
222 let term = match parse_term(term_src) {
223 Ok(t) => t,
224 Err(e) => return ReplOutcome::Error(format!("parse: {e}")),
225 };
226 let ctx = VarContext::default();
227 match typecheck_term(&term, &ctx, theory) {
228 Ok(sort) => ReplOutcome::Output(format!("{term} : {sort}")),
229 Err(e) => ReplOutcome::Error(format!("typecheck: {e}")),
230 }
231 }
232
233 fn cmd_normalize(&self, term_src: &str) -> ReplOutcome {
234 let theory = match self.active_theory() {
235 Ok(t) => t,
236 Err(e) => return e,
237 };
238 let term = match parse_term(term_src) {
239 Ok(t) => t,
240 Err(e) => return ReplOutcome::Error(format!("parse: {e}")),
241 };
242 let nf = normalize(&term, &theory.directed_eqs, 1000);
243 ReplOutcome::Output(format!("{nf}"))
244 }
245
246 fn cmd_model(&self, tail: &str) -> ReplOutcome {
247 const MAX_MODEL_DEPTH: usize = 10;
248 let theory = match self.active_theory() {
249 Ok(t) => t,
250 Err(e) => return e,
251 };
252 let depth = if tail.is_empty() {
253 3
254 } else {
255 match tail.parse::<usize>() {
256 Ok(d) if d <= MAX_MODEL_DEPTH => d,
257 Ok(d) => {
258 return ReplOutcome::Error(format!(
259 "depth {d} exceeds maximum {MAX_MODEL_DEPTH}; free-model expansion is \
260 exponential so :model caps the depth to keep the REPL responsive",
261 ));
262 }
263 Err(_) => return ReplOutcome::Error(format!("invalid depth: {tail}")),
264 }
265 };
266 let cfg = FreeModelConfig {
267 max_depth: depth,
268 ..FreeModelConfig::default()
269 };
270 match free_model(theory, &cfg) {
271 Ok(result) => {
272 let mut lines: Vec<String> = Vec::new();
273 for (sort_name, carrier) in &result.model.sort_interp {
274 let preview: Vec<String> =
275 carrier.iter().take(5).map(|v| format!("{v:?}")).collect();
276 lines.push(format!(
277 "{sort_name}: [{}{suffix}]",
278 preview.join(", "),
279 suffix = if carrier.len() > 5 { ", ..." } else { "" },
280 ));
281 }
282 ReplOutcome::Output(lines.join("\n"))
283 }
284 Err(e) => ReplOutcome::Error(format!("free model failed: {e}")),
285 }
286 }
287
288 fn cmd_instance(&mut self, rest: &str) -> ReplOutcome {
289 let Some(brace_pos) = rest.find('{') else {
294 return ReplOutcome::Error(
295 "usage: :instance <class> in <target> { binding = target; ... }".to_string(),
296 );
297 };
298 let header = rest[..brace_pos].trim();
299 let body = rest[brace_pos + 1..].trim().trim_end_matches('}');
300 let Some(in_pos) = header.find(" in ") else {
301 return ReplOutcome::Error("expected `<class> in <target>` before `{`".to_string());
302 };
303 let class_name = header[..in_pos].trim();
304 let target_name = header[in_pos + 4..].trim();
305
306 let mut bindings: HashMap<String, String> = HashMap::new();
307 for entry in body.split(';') {
308 let e = entry.trim();
309 if e.is_empty() {
310 continue;
311 }
312 let Some(eq_pos) = e.find('=') else {
313 return ReplOutcome::Error(format!("binding missing `=`: {e}"));
314 };
315 let from = e[..eq_pos].trim();
316 let to = e[eq_pos + 1..].trim();
317 bindings.insert(from.to_string(), to.to_string());
318 }
319
320 let instance_name = format!("{class_name}_to_{target_name}");
321 let spec = panproto_theory_dsl::document::InstanceSpec {
322 instance: instance_name,
323 class: class_name.to_string(),
324 target: target_name.to_string(),
325 bindings,
326 };
327 let resolver = |name: &str| self.theories.get(name).cloned();
328 match panproto_theory_dsl::compile_instance::compile_instance(&spec, &resolver) {
329 Ok(morphism) => {
330 let name = morphism.name.to_string();
331 self.morphisms.insert(name.clone(), morphism);
332 ReplOutcome::Output(format!("instance {name} ok"))
333 }
334 Err(e) => ReplOutcome::Error(format!("instance check failed: {e}")),
335 }
336 }
337
338 fn handle_term_typecheck(&self, src: &str) -> ReplOutcome {
339 self.cmd_type(src)
340 }
341}
342
343fn split_first_word(s: &str) -> (&str, &str) {
344 s.find(char::is_whitespace)
345 .map_or((s, ""), |i| (s[..i].trim(), s[i..].trim()))
346}