Skip to main content

graphix_shell/
lib.rs

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