Skip to main content

graphix_shell/
lib.rs

1use anyhow::{bail, Context, Result};
2use arcstr::ArcStr;
3use derive_builder::Builder;
4use enumflags2::BitFlags;
5use fxhash::FxHashMap;
6use graphix_compiler::{
7    env::Env,
8    expr::{CouldNotResolve, ExprId, ModuleResolver, Source},
9    format_with_flags,
10    typ::TVal,
11    CFlag, ExecCtx, PrintFlag,
12};
13use graphix_rt::{CompExp, GXConfig, GXEvent, GXExt, GXHandle, GXRt};
14use input::InputReader;
15use netidx::{
16    publisher::{Publisher, Value},
17    subscriber::Subscriber,
18};
19use poolshark::global::GPooled;
20use reedline::Signal;
21use std::{marker::PhantomData, process::exit, time::Duration};
22use tokio::{select, sync::mpsc};
23
24mod completion;
25mod deps;
26mod input;
27
28enum Output<X: GXExt> {
29    None,
30    EmptyScript,
31    Custom(deps::Cdc<X>),
32    Text(CompExp<X>),
33}
34
35impl<X: GXExt> Output<X> {
36    fn from_expr(gx: &GXHandle<X>, env: &Env, e: CompExp<X>) -> Self {
37        match deps::maybe_init_custom(gx, env, e) {
38            Err(e) => {
39                eprintln!("error initializing custom display: {e:?}");
40                Self::None
41            }
42            Ok(deps::CustomResult::Custom(cdc)) => Self::Custom(cdc),
43            Ok(deps::CustomResult::NotCustom(e)) => Self::Text(e),
44        }
45    }
46
47    async fn clear(&mut self) {
48        match self {
49            Self::None | Self::Text(_) | Self::EmptyScript => (),
50            Self::Custom(cdc) => cdc.custom.clear().await,
51        }
52        *self = Self::None
53    }
54
55    async fn process_update(&mut self, env: &Env, id: ExprId, v: Value) {
56        match self {
57            Self::None | Output::EmptyScript => (),
58            Self::Custom(cdc) => cdc.custom.process_update(env, id, v).await,
59            Self::Text(e) => {
60                if e.id == id {
61                    println!("{}", TVal { env: &env, typ: &e.typ, v: &v })
62                }
63            }
64        }
65    }
66}
67
68#[derive(Debug, Clone)]
69pub enum Mode {
70    /// Read input line by line from the user and compile/execute it.
71    /// provide completion and print the value of the last expression
72    /// as it executes. Ctrl-C cancel's execution of the last
73    /// expression and Ctrl-D exits the shell.
74    Repl,
75    /// Load compile and execute a file. Print the value
76    /// of the last expression in the file to stdout. Ctrl-C exits the
77    /// shell.
78    Script(Source),
79    /// Check that the specified file compiles but do not run it
80    Check(Source),
81}
82
83impl Mode {
84    fn file_mode(&self) -> bool {
85        match self {
86            Self::Repl => false,
87            Self::Script(_) | Self::Check(_) => true,
88        }
89    }
90}
91
92#[derive(Builder)]
93#[builder(pattern = "owned")]
94pub struct Shell<X: GXExt> {
95    /// do not run the users init module
96    #[builder(default = "false")]
97    no_init: bool,
98    /// drop subscribers if they don't consume updates after this timeout
99    #[builder(setter(strip_option), default)]
100    publish_timeout: Option<Duration>,
101    /// module resolution from netidx will fail if it can't subscribe
102    /// before this time elapses
103    #[builder(setter(strip_option), default)]
104    resolve_timeout: Option<Duration>,
105    /// define module resolvers to append to the default list
106    #[builder(default)]
107    module_resolvers: Vec<ModuleResolver>,
108    /// set the shell's mode
109    #[builder(default = "Mode::Repl")]
110    mode: Mode,
111    /// The netidx publisher to use. If you do not wish to use netidx
112    /// you can use netidx::InternalOnly to create an internal netidx
113    /// environment
114    publisher: Publisher,
115    /// The netidx subscriber to use. If you do not wish to use netidx
116    /// you can use netidx::InternalOnly to create an internal netidx
117    /// environment
118    subscriber: Subscriber,
119    /// Enable compiler flags, these will be ORed with the default set of flags
120    /// for the mode.
121    #[builder(default)]
122    enable_flags: BitFlags<CFlag>,
123    /// Disable compiler flags, these will be subtracted from the final set.
124    /// (default_flags | enable_flags) - disable_flags
125    #[builder(default)]
126    disable_flags: BitFlags<CFlag>,
127    #[builder(setter(skip), default)]
128    _phantom: PhantomData<X>,
129}
130
131impl<X: GXExt> Shell<X> {
132    async fn init(
133        &mut self,
134        sub: mpsc::Sender<GPooled<Vec<GXEvent>>>,
135    ) -> Result<GXHandle<X>> {
136        let publisher = self.publisher.clone();
137        let subscriber = self.subscriber.clone();
138        let mut ctx = ExecCtx::new(GXRt::<X>::new(publisher, subscriber))
139            .context("creating graphix context")?;
140        let mut vfs_modules = FxHashMap::default();
141        let result = deps::register::<X>(&mut ctx, &mut vfs_modules)
142            .context("register package modules")?;
143        if let Some(main) = result.main_program {
144            if matches!(self.mode, Mode::Repl) {
145                self.mode = Mode::Script(Source::Internal(ArcStr::from(main)));
146            }
147        }
148        let mut flags = match self.mode {
149            Mode::Script(_) | Mode::Check(_) => CFlag::WarnUnhandled | CFlag::WarnUnused,
150            Mode::Repl => BitFlags::empty(),
151        };
152        flags.insert(self.enable_flags);
153        flags.remove(self.disable_flags);
154        let mut mods = vec![ModuleResolver::VFS(vfs_modules)];
155        for res in self.module_resolvers.drain(..) {
156            mods.push(res);
157        }
158        let mut gx = GXConfig::builder(ctx, sub);
159        gx = gx.flags(flags);
160        if let Some(s) = self.publish_timeout {
161            gx = gx.publish_timeout(s);
162        }
163        if let Some(s) = self.resolve_timeout {
164            gx = gx.resolve_timeout(s);
165        }
166        Ok(gx
167            .root(result.root)
168            .resolvers(mods)
169            .build()
170            .context("building rt config")?
171            .start()
172            .await
173            .context("loading initial modules")?)
174    }
175
176    async fn load_env(
177        &mut self,
178        gx: &GXHandle<X>,
179        newenv: &mut Option<Env>,
180        output: &mut Output<X>,
181        exprs: &mut Vec<CompExp<X>>,
182    ) -> Result<Env> {
183        let env;
184        match &self.mode {
185            Mode::Check(source) => {
186                gx.check(source.clone()).await?;
187                exit(0)
188            }
189            Mode::Script(source) => {
190                let r = gx.load(source.clone()).await?;
191                exprs.extend(r.exprs);
192                env = gx.get_env().await?;
193                if let Some(e) = exprs.pop() {
194                    *output = Output::from_expr(&gx, &env, e);
195                }
196                *newenv = None
197            }
198            Mode::Repl if !self.no_init => match gx.compile("mod init".into()).await {
199                Ok(res) => {
200                    env = res.env;
201                    exprs.extend(res.exprs);
202                    *newenv = Some(env.clone())
203                }
204                Err(e) if e.is::<CouldNotResolve>() => {
205                    env = gx.get_env().await?;
206                    *newenv = Some(env.clone())
207                }
208                Err(e) => {
209                    eprintln!("error in init module: {e:?}");
210                    env = gx.get_env().await?;
211                    *newenv = Some(env.clone())
212                }
213            },
214            Mode::Repl => {
215                env = gx.get_env().await?;
216                *newenv = Some(env.clone());
217            }
218        }
219        Ok(env)
220    }
221
222    pub async fn run(mut self) -> Result<()> {
223        let (tx, mut from_gx) = mpsc::channel(100);
224        let gx = self.init(tx).await?;
225        let script = self.mode.file_mode();
226        let mut input = InputReader::new();
227        let mut output = if script { Output::EmptyScript } else { Output::None };
228        let mut newenv = None;
229        let mut exprs = vec![];
230        let mut env = self.load_env(&gx, &mut newenv, &mut output, &mut exprs).await?;
231        if !script {
232            println!("Welcome to the graphix shell");
233            println!("Press ctrl-c to cancel, ctrl-d to exit, and tab for help")
234        }
235        loop {
236            select! {
237                batch = from_gx.recv() => match batch {
238                    None => bail!("graphix runtime is dead"),
239                    Some(mut batch) => {
240                        for e in batch.drain(..) {
241                            match e {
242                                GXEvent::Updated(id, v) => {
243                                    output.process_update(&env, id, v).await
244                                },
245                                GXEvent::Env(e) => {
246                                    env = e;
247                                    newenv = Some(env.clone());
248                                }
249                            }
250                        }
251                    }
252                },
253                input = input.read_line(&mut output, &mut newenv) => {
254                    match input {
255                        Err(e) => eprintln!("error reading line {e:?}"),
256                        Ok(Signal::CtrlC) if script => break Ok(()),
257                        Ok(Signal::CtrlC) => output.clear().await,
258                        Ok(Signal::CtrlD) => break Ok(()),
259                        Ok(Signal::Success(line)) => {
260                            match gx.compile(ArcStr::from(line)).await {
261                                Err(e) => eprintln!("error: {e:?}"),
262                                Ok(res) => {
263                                    env = res.env;
264                                    newenv = Some(env.clone());
265                                    exprs.extend(res.exprs);
266                                    if exprs.last().map(|e| e.output).unwrap_or(false) {
267                                        let e = exprs.pop().unwrap();
268                                        let typ = e.typ
269                                            .with_deref(|t| t.cloned())
270                                            .unwrap_or_else(|| e.typ.clone());
271                                        format_with_flags(
272                                            PrintFlag::DerefTVars | PrintFlag::ReplacePrims,
273                                            || println!("-: {}", typ)
274                                        );
275                                        output = Output::from_expr(&gx, &env, e);
276                                    } else {
277                                        output.clear().await
278                                    }
279                                }
280                            }
281                        }
282                    }
283                },
284            }
285        }
286    }
287}