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