1use crate::session::{
9 fmt_diag, parse_decl, parse_expr, pretty_json, BindSource, EditKind, Mode, Op, Session,
10 SessionError,
11};
12use regex::Regex;
13use std::io::{BufRead, Read, Write};
14
15pub const COMMANDS: &[(&str, &str, &str)] = &[
16 (
18 ":load file.decl",
19 "open the universe from an entry module (a new session)",
20 "universe",
21 ),
22 (
23 ":reload",
24 "re-read every module of the universe from disk",
25 "universe",
26 ),
27 (
28 ":roots",
29 "the roots of the universe and of the session",
30 "universe",
31 ),
32 (
34 ":bind name=doc.json",
35 "bind a JSON file to an input",
36 "documents",
37 ),
38 (
39 ":bind name { … }",
40 "bind an inline JSON document",
41 "documents",
42 ),
43 (
44 ":bind name = expr",
45 "bind the value of an expression as the document",
46 "documents",
47 ),
48 (":unbind name", "drop the binding", "documents"),
49 (
50 ":create path = expr",
51 "add a member, entry, or element at a path of a document",
52 "documents",
53 ),
54 (
55 ":update path = expr",
56 "replace the value at a path of a document",
57 "documents",
58 ),
59 (
60 ":remove path",
61 "remove the value at a path of a document",
62 "documents",
63 ),
64 (
65 ":diff name",
66 "the document against what it started from",
67 "documents",
68 ),
69 (
70 ":save name=file",
71 "write the document of a root to a file",
72 "documents",
73 ),
74 (":drop name", "remove a session declaration", "declarations"),
76 (
77 ":write file.decl",
78 "write the scratch module to a file",
79 "declarations",
80 ),
81 (
82 ":session",
83 "the session's declarations and documents",
84 "declarations",
85 ),
86 (
87 ":reset",
88 "drop every binding, edit, and declaration",
89 "declarations",
90 ),
91 (":check", "static diagnostics of every module", "evaluation"),
93 (
94 ":evaluate [root…]",
95 "full evaluation: the documents of the roots",
96 "evaluation",
97 ),
98 (
99 ":validate [root…]",
100 "full validation: every diagnostic, then a verdict per root",
101 "evaluation",
102 ),
103 (
104 ":fmt",
105 "the scratch module, canonically formatted",
106 "evaluation",
107 ),
108 (
110 ":type expr",
111 "the static type of an expression",
112 "inspection",
113 ),
114 (
115 ":doc name",
116 "a declaration and its documentation",
117 "inspection",
118 ),
119 (":path expr", "the canonical path of a place", "inspection"),
120 (
121 ":trace path",
122 "the derivation of a place, or its root cause",
123 "inspection",
124 ),
125 (
126 ":complete text",
127 "the completions offered at the end of the text",
128 "inspection",
129 ),
130 (":undo [n]", "step the log back", "history"),
132 (":redo [n]", "step forward again", "history"),
133 (
134 ":history [file]",
135 "the log, or write it as a session file",
136 "history",
137 ),
138 (":time", "wall time of the last evaluation", "session"),
140 (":set pretty|compact", "value printing", "session"),
141 (":help [command]", "these commands", "session"),
142 (":quit", "end the session", "session"),
143];
144
145fn command_names() -> Vec<&'static str> {
146 let mut out: Vec<&'static str> = vec![];
147 for c in COMMANDS {
148 let n = c.0.split(' ').next().unwrap_or("");
149 if !out.contains(&n) {
150 out.push(n);
151 }
152 }
153 out
154}
155
156const KEYWORDS: &[&str] = &[
157 "if", "then", "else", "for", "in", "match", "with", "matches", "true", "false", "null",
158 "export",
159];
160
161fn is_decl_head(t: &str) -> bool {
162 Regex::new(
163 r"^\s*(?:export\s+)?(type|const|func|output|input|diagnostic|dimension|unit|import)\b",
164 )
165 .unwrap()
166 .is_match(t)
167}
168fn is_ident(s: &str) -> bool {
169 Regex::new(r"^[A-Za-z_][A-Za-z0-9_]*$").unwrap().is_match(s)
170}
171
172fn output_head(t: &str) -> Option<(String, Option<String>, String)> {
174 let re = Regex::new(r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*").unwrap();
175 let m = re.captures(t)?;
176 let name = m.get(1).unwrap().as_str().to_string();
177 let mut rest = &t[m.get(0).unwrap().end()..];
178 let mut ty: Option<String> = None;
179 if let Some(r) = rest.strip_prefix(':') {
180 let r = r.trim_start();
181 let eq = r.find('=')?;
182 let type_part = &r[..eq];
183 if type_part.trim_end().is_empty() {
184 return None;
185 }
186 ty = Some(type_part.trim().to_string());
187 rest = &r[eq..];
188 }
189 let rest = rest.strip_prefix('=')?;
190 if rest.starts_with('=') {
191 return None;
192 }
193 let expr = rest.trim_start();
194 if expr.is_empty() {
195 return None;
196 }
197 Some((name, ty, expr.trim().to_string()))
198}
199
200pub fn needs_more(text: &str) -> bool {
202 let cs: Vec<char> = text.chars().collect();
203 let mut depth = 0i32;
204 let mut in_str: Option<char> = None;
205 let mut i = 0;
206 while i < cs.len() {
207 let c = cs[i];
208 if let Some(q) = in_str {
209 if c == '\\' {
210 i += 1;
211 } else if c == q {
212 in_str = None;
213 }
214 i += 1;
215 continue;
216 }
217 if c == '"' || c == '`' {
218 in_str = Some(c);
219 } else if c == '/' && cs.get(i + 1) == Some(&'/') {
220 match cs[i..].iter().position(|&x| x == '\n') {
221 Some(nl) => i += nl,
222 None => break,
223 }
224 } else if "{[(".contains(c) {
225 depth += 1;
226 } else if "}])".contains(c) {
227 depth -= 1;
228 }
229 i += 1;
230 }
231 if depth > 0 || in_str == Some('`') {
232 return true;
233 }
234 if text.trim_start().starts_with(':') {
235 return false; }
237 let no_comment = Regex::new(r"//[^\n]*$")
238 .unwrap()
239 .replace(text, "")
240 .to_string();
241 let tail = no_comment.trim_end();
242 Regex::new(r"(?:[+\-*/%<>=!&|?:,]|\bthen|\belse|\bin|\bwith|=>)$")
243 .unwrap()
244 .is_match(tail)
245}
246
247pub struct Repl {
248 pub session: Session,
249 pub compact: bool,
250 pub errors: usize,
251 pub quit_requested: bool,
252 out: Box<dyn Fn(&str)>,
253 buffer: Vec<String>,
254}
255
256impl Repl {
257 pub fn new(out: Box<dyn Fn(&str)>, entry: Option<&str>) -> Repl {
258 Repl {
259 session: Session::new(entry),
260 compact: false,
261 errors: 0,
262 quit_requested: false,
263 out,
264 buffer: vec![],
265 }
266 }
267
268 pub fn line(&mut self, text: &str) -> bool {
270 self.buffer.push(text.to_string());
271 let whole = self.buffer.join("\n");
272 if needs_more(&whole) {
273 return false;
274 }
275 self.buffer.clear();
276 self.input(&whole);
277 true
278 }
279 pub fn discard(&mut self) {
281 self.buffer.clear();
282 }
283 pub fn pending(&self) -> bool {
284 !self.buffer.is_empty()
285 }
286
287 fn out(&self, line: &str) {
288 (self.out)(line)
289 }
290 fn error(&mut self, msg: &str) {
291 self.errors += 1;
292 self.out(&format!("error: {msg}"));
293 }
294 fn diag(&self, d: &crate::semantics::Diag, in_file: Option<&str>) {
295 self.out(&fmt_diag(d, in_file));
296 }
297 fn value(&self, json: &str) {
298 if self.compact {
299 self.out(json);
300 } else {
301 self.out(&pretty_json(json));
302 }
303 }
304
305 pub fn input(&mut self, text: &str) {
306 let t = text.trim();
307 if t.is_empty() || (t.starts_with("//") && !t.starts_with("///")) {
308 return;
309 }
310 let r = if t.starts_with(':') {
311 self.command(t)
312 } else if is_decl_head(t) {
313 self.add_declaration(t)
314 } else if let Some((name, ty, expr)) =
315 output_head(t).filter(|(n, _, _)| !KEYWORDS.contains(&n.as_str()))
316 {
317 self.session_output(&name, ty, &expr)
318 } else {
319 self.expression(t)
320 };
321 if let Err(e) = r {
322 self.error(&e.0);
323 }
324 }
325
326 fn expression(&mut self, text: &str) -> Result<(), SessionError> {
327 parse_expr(text)?;
328 let r = self.session.evaluate_expr(text)?;
329 for d in &r.diags {
330 self.diag(d, None);
331 }
332 match (&r.error, &r.value) {
333 (Some((code, message)), _) => {
334 if !message.is_empty() {
335 self.out(&format!(
336 "error{}: {message}",
337 code.as_ref().map(|c| format!(" [{c}]")).unwrap_or_default()
338 ));
339 }
340 self.out("(invalid)");
341 }
342 (None, Some(v)) => self.value(v),
343 (None, None) => self.out("(invalid)"),
344 }
345 self.out("(partial)");
346 Ok(())
347 }
348 fn add_declaration(&mut self, text: &str) -> Result<(), SessionError> {
349 let (_, name) = parse_decl(text)?;
350 self.session.apply(Op::Declare {
351 name,
352 text: text.trim().to_string(),
353 })
354 }
355 fn session_output(
356 &mut self,
357 name: &str,
358 ty: Option<String>,
359 expr: &str,
360 ) -> Result<(), SessionError> {
361 parse_expr(expr)?;
362 if let Some(t) = &ty {
363 parse_decl(&format!("output {name}: {t} = 0"))?;
364 }
365 self.session.apply(Op::Output {
366 name: name.to_string(),
367 ty,
368 expr: expr.to_string(),
369 })
370 }
371
372 fn command(&mut self, t: &str) -> Result<(), SessionError> {
373 let sp = t.find(char::is_whitespace);
374 let cmd = match sp {
375 Some(i) => &t[..i],
376 None => t,
377 };
378 let rest = match sp {
379 Some(i) => t[i + 1..].trim(),
380 None => "",
381 }
382 .to_string();
383 let cmd = cmd.to_string();
384 let no_args = |rest: &str| -> Result<(), SessionError> {
385 if !rest.is_empty() {
386 Err(SessionError(format!("{cmd} takes no argument")))
387 } else {
388 Ok(())
389 }
390 };
391 let one_name = |rest: &str| -> Result<String, SessionError> {
392 if !is_ident(rest) {
393 Err(SessionError(format!("{cmd} expects a name")))
394 } else {
395 Ok(rest.to_string())
396 }
397 };
398 let entry_abs = self.session.entry_abs().display().to_string();
399 let in_file = |file: &str| -> Option<String> {
400 if file == entry_abs {
401 None
402 } else {
403 Some(file.to_string())
404 }
405 };
406 match cmd.as_str() {
407 ":load" => {
408 if rest.is_empty() {
409 return Err(SessionError(":load expects a file".into()));
410 }
411 self.session = Session::new(Some(&rest));
412 Ok(())
413 }
414 ":reload" => {
415 no_args(&rest)?;
416 let op = self.session.reload_op();
417 self.session.apply(op)
418 }
419 ":roots" => {
420 no_args(&rest)?;
421 let rs = self.session.roots();
422 if rs.is_empty() {
423 self.out("(no roots)");
424 return Ok(());
425 }
426 for r in rs {
427 let status = if r.session {
428 "session".to_string()
429 } else if r.kind == "output" {
430 if r.binding == "detached" {
431 "detached".into()
432 } else if r.exported {
433 "exported".into()
434 } else {
435 "local".into()
436 }
437 } else {
438 r.binding.clone()
439 };
440 let line = format!(
441 "{:<7} {:<16} {:<12} {:<16} {}{}",
442 r.kind,
443 r.name,
444 status,
445 r.module,
446 r.detail,
447 if r.edited { " (edited)" } else { "" }
448 );
449 self.out(line.trim_end());
450 }
451 Ok(())
452 }
453 ":bind" => {
454 let eq_re = Regex::new(r"^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([\s\S]+)$").unwrap();
455 if let Some(m) = eq_re.captures(&rest) {
456 let name = m.get(1).unwrap().as_str().to_string();
457 let val = m.get(2).unwrap().as_str();
458 let after_name = &rest[name.len()..];
459 let starts_bracket = val.trim().starts_with('[') || val.trim().starts_with('{');
460 if !starts_bracket && !after_name.starts_with(char::is_whitespace) {
461 let file = val.trim().to_string();
463 let text = std::fs::read_to_string(&file)
464 .map_err(|_| SessionError(format!("cannot read {file}")))?;
465 return self.session.apply(Op::Bind {
466 name,
467 src: BindSource::File { file, text },
468 });
469 }
470 return self.session.apply(Op::Bind {
471 name,
472 src: BindSource::Expr {
473 text: val.trim().to_string(),
474 },
475 });
476 }
477 let inline_re = Regex::new(r"^([A-Za-z_][A-Za-z0-9_]*)\s+([\[{][\s\S]*)$").unwrap();
478 if let Some(m) = inline_re.captures(&rest) {
479 let name = m.get(1).unwrap().as_str().to_string();
480 let text = m.get(2).unwrap().as_str().to_string();
481 return self.session.apply(Op::Bind {
482 name,
483 src: BindSource::Inline { text },
484 });
485 }
486 Err(SessionError(
487 ":bind expects name=doc.json, name { … }, or name = expr".into(),
488 ))
489 }
490 ":unbind" => {
491 let name = one_name(&rest)?;
492 self.session.apply(Op::Unbind { name })
493 }
494 ":create" | ":update" => {
495 let re = Regex::new(r"^(\S+)\s*=\s*([\s\S]+)$").unwrap();
496 let Some(m) = re.captures(&rest) else {
497 return Err(SessionError(format!("{cmd} expects path = expr")));
498 };
499 let kind = if cmd == ":create" {
500 EditKind::Create
501 } else {
502 EditKind::Update
503 };
504 self.session.apply(Op::Edit {
505 kind,
506 path: m.get(1).unwrap().as_str().to_string(),
507 expr: Some(m.get(2).unwrap().as_str().trim().to_string()),
508 })
509 }
510 ":remove" => {
511 if rest.is_empty() || rest.contains(char::is_whitespace) {
512 return Err(SessionError(":remove expects a path".into()));
513 }
514 self.session.apply(Op::Edit {
515 kind: EditKind::Remove,
516 path: rest.clone(),
517 expr: None,
518 })
519 }
520 ":diff" => {
521 let name = one_name(&rest)?;
522 for l in self.session.diff(&name)? {
523 self.out(&l);
524 }
525 Ok(())
526 }
527 ":save" => {
528 let re = Regex::new(r"^([A-Za-z_][A-Za-z0-9_]*)=(\S+)$").unwrap();
529 let Some(m) = re.captures(&rest) else {
530 return Err(SessionError(":save expects name=file".into()));
531 };
532 self.session
533 .save(m.get(1).unwrap().as_str(), m.get(2).unwrap().as_str())
534 }
535 ":drop" => {
536 let name = one_name(&rest)?;
537 self.session.apply(Op::Drop { name })
538 }
539 ":write" => {
540 if rest.is_empty() {
541 return Err(SessionError(":write expects a file".into()));
542 }
543 self.session.write(&rest)
544 }
545 ":session" => {
546 no_args(&rest)?;
547 let ls = self.session.session_lines();
548 if ls.is_empty() {
549 self.out("(empty session)");
550 }
551 for l in ls {
552 self.out(&l);
553 }
554 Ok(())
555 }
556 ":reset" => {
557 no_args(&rest)?;
558 self.session.apply(Op::Reset)
559 }
560 ":check" => {
561 no_args(&rest)?;
562 let cs = self.session.check();
563 for (file, d) in &cs {
564 self.diag(d, in_file(file).as_deref());
565 }
566 if cs.is_empty() {
567 self.out("ok");
568 }
569 Ok(())
570 }
571 ":evaluate" => {
572 let names: Vec<String> = if rest.is_empty() {
573 vec![]
574 } else {
575 Regex::new(r"[\s,]+")
576 .unwrap()
577 .split(&rest)
578 .filter(|s| !s.is_empty())
579 .map(|s| s.to_string())
580 .collect()
581 };
582 let (run, docs, exported) = self.session.evaluate(&names)?;
583 for d in &run.load_diags {
584 self.diag(d, None);
585 }
586 for (file, d) in &run.checks {
587 self.diag(d, in_file(file).as_deref());
588 }
589 for d in &run.session_checks {
590 self.diag(d, None);
591 }
592 for d in &run.diags {
593 self.diag(d, None);
594 }
595 if run.entry.is_none() {
596 return Ok(());
597 }
598 if run.eng.is_none() {
599 self.out("(not evaluated)");
600 return Ok(());
601 }
602 if exported {
603 if docs.iter().any(|(_, j)| j.is_none()) {
604 self.out("(invalid)");
605 return Ok(());
606 }
607 let text = format!(
608 "{{{}}}",
609 docs.iter()
610 .map(|(n, j)| format!(
611 "{}:{}",
612 crate::semantics::json_str(n),
613 j.clone().unwrap_or_default()
614 ))
615 .collect::<Vec<_>>()
616 .join(",")
617 );
618 self.value(&text);
619 return Ok(());
620 }
621 let many = docs.len() > 1;
622 for (name, json) in &docs {
623 if many {
624 self.out(&format!("{name}:"));
625 }
626 match json {
627 None => self.out("(invalid)"),
628 Some(j) => self.value(j),
629 }
630 }
631 Ok(())
632 }
633 ":validate" => {
634 let names: Vec<String> = if rest.is_empty() {
635 vec![]
636 } else {
637 Regex::new(r"[\s,]+")
638 .unwrap()
639 .split(&rest)
640 .filter(|s| !s.is_empty())
641 .map(|s| s.to_string())
642 .collect()
643 };
644 let (run, verdicts, diags) = self.session.validate(&names)?;
645 for d in &run.load_diags {
646 self.diag(d, None);
647 }
648 for (file, d) in &run.checks {
649 self.diag(d, in_file(file).as_deref());
650 }
651 for d in &run.session_checks {
652 self.diag(d, None);
653 }
654 if run.eng.is_none() {
655 self.out("(not evaluated)");
656 return Ok(());
657 }
658 for d in &diags {
659 self.diag(d, None);
660 }
661 if verdicts.is_empty() {
662 self.out("(no roots)");
663 }
664 for (name, errors, warnings) in &verdicts {
665 let n = |k: usize, w: &str| format!("{k} {w}{}", if k == 1 { "" } else { "s" });
666 if *errors == 0 && *warnings == 0 {
667 self.out(&format!("{name}: ok"));
668 } else {
669 let parts: Vec<String> = [
670 if *errors > 0 {
671 n(*errors, "error")
672 } else {
673 String::new()
674 },
675 if *warnings > 0 {
676 n(*warnings, "warning")
677 } else {
678 String::new()
679 },
680 ]
681 .into_iter()
682 .filter(|s| !s.is_empty())
683 .collect();
684 self.out(&format!("{name}: {}", parts.join(", ")));
685 }
686 }
687 Ok(())
688 }
689 ":fmt" => {
690 no_args(&rest)?;
691 let t = self.session.fmt()?;
692 if t.is_empty() {
693 self.out("(empty session)");
694 } else {
695 self.out(t.strip_suffix('\n').unwrap_or(&t));
696 }
697 Ok(())
698 }
699 ":type" => {
700 if rest.is_empty() {
701 return Err(SessionError(":type expects an expression".into()));
702 }
703 let (ty, maybe_absent, diags) = self.session.type_of(&rest)?;
704 for d in &diags {
705 self.diag(d, None);
706 }
707 self.out(&format!(
708 "{ty}{}",
709 if maybe_absent { " (maybe absent)" } else { "" }
710 ));
711 Ok(())
712 }
713 ":doc" => {
714 if rest.is_empty() {
715 return Err(SessionError(":doc expects a name".into()));
716 }
717 for l in self.session.doc_of(&rest)? {
718 self.out(&l);
719 }
720 Ok(())
721 }
722 ":path" => {
723 if rest.is_empty() {
724 return Err(SessionError(":path expects an expression".into()));
725 }
726 let p = self.session.path_of(&rest)?;
727 self.out(&p);
728 Ok(())
729 }
730 ":trace" => {
731 if rest.is_empty() || rest.contains(char::is_whitespace) {
732 return Err(SessionError(":trace expects a path".into()));
733 }
734 for l in self.session.trace(&rest)? {
735 self.out(&l);
736 }
737 Ok(())
738 }
739 ":complete" => {
740 let names = command_names();
741 let cs = self.session.complete(&rest, &names);
742 if cs.is_empty() {
743 self.out("(no completions)");
744 }
745 for c in cs {
746 self.out(&c);
747 }
748 Ok(())
749 }
750 ":undo" | ":redo" => {
751 let n = if rest.is_empty() {
752 Some(1)
753 } else {
754 js_parse_int(&rest)
755 };
756 let Some(n) = n.filter(|n| *n >= 1) else {
757 return Err(SessionError(format!("{cmd} expects a count")));
758 };
759 let k = if cmd == ":undo" {
760 self.session.undo(n as usize)
761 } else {
762 self.session.redo(n as usize)
763 };
764 if k == 0 {
765 self.out(if cmd == ":undo" {
766 "nothing to undo"
767 } else {
768 "nothing to redo"
769 });
770 }
771 Ok(())
772 }
773 ":history" => {
774 if !rest.is_empty() {
775 let text = format!("{}\n", self.session.script_lines().join("\n"));
776 return std::fs::write(&rest, text)
777 .map_err(|_| SessionError(format!("cannot write {rest}")));
778 }
779 for l in self.session.history_lines() {
780 self.out(&l);
781 }
782 Ok(())
783 }
784 ":time" => {
785 no_args(&rest)?;
786 match self.session.last_timing.get() {
787 None => self.out("nothing evaluated yet"),
788 Some(t) => {
789 let step = match (t.recomputed, t.slots) {
790 (Some(n), Some(m)) => format!(", recomputed {n} of {m} slots"),
791 _ => String::new(),
792 };
793 self.out(&format!("total {:.1} ms (load {:.1} ms, check {:.1} ms, bind {:.1} ms, evaluate {:.1} ms){step}", t.total, t.load, t.check, t.bind, t.evaluate))
794 }
795 }
796 Ok(())
797 }
798 ":set" => {
799 match rest.as_str() {
800 "pretty" => self.compact = false,
801 "compact" => self.compact = true,
802 _ => return Err(SessionError(":set expects pretty or compact".into())),
803 }
804 Ok(())
805 }
806 ":help" => {
807 let want = if rest.is_empty() {
808 None
809 } else {
810 Some(if rest.starts_with(':') {
811 rest.clone()
812 } else {
813 format!(":{rest}")
814 })
815 };
816 let rows: Vec<&(&str, &str, &str)> = COMMANDS
817 .iter()
818 .filter(|c| {
819 want.as_ref()
820 .map(|w| c.0.split(' ').next() == Some(w.as_str()))
821 .unwrap_or(true)
822 })
823 .collect();
824 if rows.is_empty() {
825 return Err(SessionError(format!("unknown command {rest}")));
826 }
827 let mut cat = "";
828 for (form, what, c) in rows {
829 if want.is_none() && *c != cat {
830 cat = c;
831 self.out(&format!("{cat}:"));
832 }
833 self.out(&format!(" {:<24} {what}", form));
834 }
835 Ok(())
836 }
837 ":quit" => {
838 no_args(&rest)?;
839 self.quit_requested = true;
840 Ok(())
841 }
842 _ => Err(SessionError(format!("unknown command {cmd}"))),
843 }
844 }
845}
846
847fn js_parse_int(s: &str) -> Option<i64> {
849 let t = s.trim_start();
850 let (neg, digits) = match t.strip_prefix('-') {
851 Some(r) => (true, r),
852 None => (false, t.strip_prefix('+').unwrap_or(t)),
853 };
854 let n: String = digits.chars().take_while(|c| c.is_ascii_digit()).collect();
855 if n.is_empty() {
856 return None;
857 }
858 let v: i64 = n.parse().ok()?;
859 Some(if neg { -v } else { v })
860}
861
862pub fn run_repl(args: Vec<String>) -> i32 {
864 let mut entry: Option<String> = None;
865 let mut script: Option<String> = None;
866 let mut compact = false;
867 let mut inputs: Vec<String> = vec![];
868 let mut i = 0;
869 while i < args.len() {
870 let a = &args[i];
871 if a == "--script" {
872 i += 1;
873 script = args.get(i).cloned();
874 } else if a == "--input" {
875 i += 1;
876 if let Some(s) = args.get(i) {
877 inputs.push(s.clone());
878 }
879 } else if a == "--compact" {
880 compact = true;
881 } else if a.starts_with("--") {
882 eprintln!("unknown option {a}");
883 return 2;
884 } else if entry.is_none() {
885 entry = Some(a.clone());
886 } else {
887 eprintln!("decl repl takes one entry file");
888 return 2;
889 }
890 i += 1;
891 }
892 if script.is_none() && entry.is_none() && !inputs.is_empty() {
893 eprintln!("--input needs an entry file");
894 return 2;
895 }
896 for spec in &inputs {
897 if !spec.contains('=') {
898 eprintln!("--input expects name=doc.json, got {spec}");
899 return 2;
900 }
901 }
902
903 let out: Box<dyn Fn(&str)> = Box::new(|l: &str| {
904 let stdout = std::io::stdout();
905 let mut h = stdout.lock();
906 let _ = writeln!(h, "{l}");
907 });
908 let mut repl = Repl::new(out, entry.as_deref());
909 repl.compact = compact;
910 for spec in &inputs {
911 repl.input(&format!(":bind {spec}"));
912 }
913
914 if let Some(script) = script {
915 let text = if script == "-" {
916 let mut s = String::new();
917 std::io::stdin().read_to_string(&mut s).map(|_| s).ok()
918 } else {
919 std::fs::read_to_string(&script).ok()
920 };
921 let Some(text) = text else {
922 eprintln!("cannot read {script}");
923 return 2;
924 };
925 let text = text.strip_suffix('\n').unwrap_or(&text).to_string();
926 for l in text.split('\n') {
927 let prompt = if repl.pending() { ". " } else { "> " };
928 println!("{prompt}{l}");
929 repl.line(l);
930 if repl.quit_requested {
931 break;
932 }
933 }
934 if repl.pending() {
935 repl.line("");
936 }
937 return if repl.errors > 0 { 1 } else { 0 };
938 }
939
940 let repl = std::rc::Rc::new(std::cell::RefCell::new(repl));
943 let config = rustyline::Config::builder()
945 .completion_type(rustyline::CompletionType::List)
946 .build();
947 let mut rl: rustyline::Editor<DeclHelper, rustyline::history::DefaultHistory> =
948 match rustyline::Editor::with_config(config) {
949 Ok(e) => e,
950 Err(_) => return run_plain(repl),
951 };
952 rl.set_helper(Some(DeclHelper { repl: repl.clone() }));
953 let history =
954 std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".decl_history"));
955 if let Some(h) = &history {
956 let _ = rl.load_history(h);
957 }
958 loop {
959 let prompt = if repl.borrow().pending() { ". " } else { "> " };
960 match rl.readline(prompt) {
961 Ok(l) => {
962 if !l.trim().is_empty() {
963 let _ = rl.add_history_entry(l.as_str());
964 }
965 repl.borrow_mut().line(&l);
966 if repl.borrow().quit_requested {
967 break;
968 }
969 }
970 Err(rustyline::error::ReadlineError::Interrupted) => {
971 repl.borrow_mut().discard();
972 continue;
973 }
974 Err(_) => break,
975 }
976 }
977 if let Some(h) = &history {
978 let _ = rl.save_history(h);
979 }
980 let _ = Mode::Full;
981 0
982}
983
984fn run_plain(repl: std::rc::Rc<std::cell::RefCell<Repl>>) -> i32 {
986 let stdin = std::io::stdin();
987 let mut lines = stdin.lock().lines();
988 loop {
989 print!("{}", if repl.borrow().pending() { ". " } else { "> " });
990 let _ = std::io::stdout().flush();
991 let Some(Ok(l)) = lines.next() else { break };
992 repl.borrow_mut().line(&l);
993 if repl.borrow().quit_requested {
994 break;
995 }
996 }
997 0
998}
999
1000struct DeclHelper {
1003 repl: std::rc::Rc<std::cell::RefCell<Repl>>,
1004}
1005impl rustyline::completion::Completer for DeclHelper {
1006 type Candidate = rustyline::completion::Pair;
1007 fn complete(
1008 &self,
1009 line: &str,
1010 pos: usize,
1011 _ctx: &rustyline::Context<'_>,
1012 ) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
1013 let head = &line[..pos];
1014 let candidates: Vec<String> = self
1015 .repl
1016 .borrow_mut()
1017 .session
1018 .complete(head, &command_names())
1019 .into_iter()
1020 .map(|c| c.split(" ").next().unwrap_or("").to_string())
1021 .collect();
1022 let tok_start = head
1023 .rfind(|c: char| !(c.is_alphanumeric() || "_$:.[]\"".contains(c)))
1024 .map(|i| i + 1)
1025 .unwrap_or(0);
1026 let tok = &head[tok_start..];
1027 let tail_start = tok
1028 .rfind('.')
1029 .map(|i| tok_start + i + 1)
1030 .unwrap_or(tok_start);
1031 let tail = &head[tail_start..];
1032 let pairs = candidates
1033 .into_iter()
1034 .filter(|c| c.starts_with(tail))
1035 .map(|c| rustyline::completion::Pair {
1036 display: c.clone(),
1037 replacement: c,
1038 })
1039 .collect();
1040 Ok((tail_start, pairs))
1041 }
1042}
1043impl rustyline::hint::Hinter for DeclHelper {
1044 type Hint = String;
1045}
1046impl rustyline::highlight::Highlighter for DeclHelper {}
1047impl rustyline::validate::Validator for DeclHelper {}
1048impl rustyline::Helper for DeclHelper {}