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