Skip to main content

graphix_rt/
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)]
5//! A general purpose graphix runtime
6//!
7//! This module implements a generic graphix runtime suitable for most
8//! applications, including applications that implement custom graphix
9//! builtins. The graphix interperter is run in a background task, and
10//! can be interacted with via a handle. All features of the standard
11//! library are supported by this runtime.
12use anyhow::{anyhow, bail, Result};
13use arcstr::ArcStr;
14use derive_builder::Builder;
15use enumflags2::BitFlags;
16use fxhash::FxHashSet;
17use graphix_compiler::{
18    env::Env,
19    expr::{ExprId, ModPath, ModuleResolver, Source},
20    typ::{FnType, Type},
21    BindId, CFlag, Event, ExecCtx, NoUserEvent, Scope, UserEvent,
22};
23use log::error;
24use netidx::{
25    protocol::valarray::ValArray,
26    publisher::{Value, WriteRequest},
27    subscriber::{self, SubId},
28};
29use netidx_core::atomic_id;
30use netidx_value::FromValue;
31use poolshark::global::GPooled;
32use serde_derive::{Deserialize, Serialize};
33use smallvec::SmallVec;
34use std::{fmt, future, sync::Arc, time::Duration};
35use tokio::{
36    sync::{
37        mpsc::{self as tmpsc},
38        oneshot,
39    },
40    task::{self, JoinHandle},
41};
42
43mod gx;
44mod rt;
45use gx::GX;
46pub use rt::GXRt;
47
48/// Trait to extend the event loop
49///
50/// The Graphix event loop has two steps,
51/// - update event sources, polls external async event sources like
52///   netidx, sockets, files, etc
53/// - do cycle, collects all the events and delivers them to the dataflow
54///   graph as a batch of "everything that happened"
55///
56/// As such to extend the event loop you must implement two things. A function
57/// to poll your own external event sources, and a function to take the events
58/// you got from those sources and represent them to the dataflow graph. You
59/// represent them either by setting generic variables (bindid -> value map), or
60/// by setting some custom structures that you define as part of your UserEvent
61/// implementation.
62///
63/// Your Graphix builtins can access both your custom structure, to register new
64/// event sources, etc, and your custom user event structure, to receive events
65/// who's types do not fit nicely as `Value`. If your event payload does fit
66/// nicely as a `Value`, then just use a variable.
67pub trait GXExt: Default + fmt::Debug + Send + Sync + 'static {
68    type UserEvent: UserEvent + Send + Sync + 'static;
69
70    /// Update your custom event sources
71    ///
72    /// Your `update_sources` MUST be cancel safe.
73    fn update_sources(&mut self) -> impl Future<Output = Result<()>> + Send;
74
75    /// Collect events that happened and marshal them into the event structure
76    ///
77    /// for delivery to the dataflow graph. `do_cycle` will be called, and a
78    /// batch of events delivered to the graph until `is_ready` returns false.
79    /// It is possible that a call to `update_sources` will result in
80    /// multiple calls to `do_cycle`, but it is not guaranteed that
81    /// `update_sources` will not be called again before `is_ready`
82    /// returns false.
83    fn do_cycle(&mut self, event: &mut Event<Self::UserEvent>) -> Result<()>;
84
85    /// Return true if there are events ready to deliver
86    fn is_ready(&self) -> bool;
87
88    /// Clear the state
89    fn clear(&mut self);
90
91    /// Create and return an empty custom event structure
92    fn empty_event(&mut self) -> Self::UserEvent;
93}
94
95#[derive(Debug, Default)]
96pub struct NoExt;
97
98impl GXExt for NoExt {
99    type UserEvent = NoUserEvent;
100
101    async fn update_sources(&mut self) -> Result<()> {
102        future::pending().await
103    }
104
105    fn do_cycle(&mut self, _event: &mut Event<Self::UserEvent>) -> Result<()> {
106        Ok(())
107    }
108
109    fn is_ready(&self) -> bool {
110        false
111    }
112
113    fn clear(&mut self) {}
114
115    fn empty_event(&mut self) -> Self::UserEvent {
116        NoUserEvent
117    }
118}
119
120type UpdateBatch = GPooled<Vec<(SubId, subscriber::Event)>>;
121type WriteBatch = GPooled<Vec<WriteRequest>>;
122
123#[derive(Debug)]
124pub struct CompExp<X: GXExt> {
125    pub id: ExprId,
126    pub typ: Type,
127    pub output: bool,
128    rt: GXHandle<X>,
129}
130
131impl<X: GXExt> Drop for CompExp<X> {
132    fn drop(&mut self) {
133        let _ = self.rt.0.tx.send(ToGX::Delete { id: self.id });
134    }
135}
136
137#[derive(Debug)]
138pub struct CompRes<X: GXExt> {
139    pub exprs: SmallVec<[CompExp<X>; 1]>,
140    pub env: Env,
141}
142
143/// Result of a typecheck-only compile pass. Carries the env as it
144/// would be after the source was compiled, plus the set of resolved
145/// name references and module references encountered during
146/// compilation. The IDE-side collections are `GPooled` so the buffers
147/// return to the runtime-side named pools after crossing the LSP
148/// thread boundary, keeping the recompile-per-keystroke loop
149/// allocation-free in steady state.
150#[derive(Debug)]
151pub struct CheckResult {
152    pub env: Env,
153    pub references: GPooled<Vec<graphix_compiler::ReferenceSite>>,
154    pub module_references: GPooled<Vec<graphix_compiler::ModuleRefSite>>,
155    pub scope_map: GPooled<Vec<graphix_compiler::ScopeMapEntry>>,
156    /// IDE side-channels populated only in `lsp_mode`: type references,
157    /// sig→impl bind links, and per-module impl-side env snapshots.
158    /// Empty for non-LSP compiles.
159    pub lsp: graphix_compiler::env::Lsp,
160}
161
162pub struct Ref<X: GXExt> {
163    pub id: ExprId,
164    // the most recent value of the variable
165    pub last: Option<Value>,
166    pub bid: BindId,
167    pub target_bid: Option<BindId>,
168    pub typ: Type,
169    rt: GXHandle<X>,
170}
171
172impl<X: GXExt> Drop for Ref<X> {
173    fn drop(&mut self) {
174        let _ = self.rt.0.tx.send(ToGX::Delete { id: self.id });
175    }
176}
177
178impl<X: GXExt> Ref<X> {
179    /// set the value of the ref `r <-`
180    ///
181    /// This will cause all nodes dependent on this id to update. This is the
182    /// same thing as the `<-` operator in Graphix. This does the same thing as
183    /// `GXHandle::set`
184    pub fn set<T: Into<Value>>(&mut self, v: T) -> Result<()> {
185        let v = v.into();
186        self.last = Some(v.clone());
187        self.rt.set(self.bid, v)
188    }
189
190    /// set the value pointed to by ref `*r <-`
191    ///
192    /// This will cause all nodes dependent on *id to update. This is the same
193    /// as the `*r <-` operator in Graphix. This does the same thing as
194    /// `GXHandle::set` using the target id.
195    pub fn set_deref<T: Into<Value>>(&mut self, v: T) -> Result<()> {
196        if let Some(id) = self.target_bid {
197            self.rt.set(id, v)?
198        }
199        Ok(())
200    }
201
202    /// Process an update
203    ///
204    /// If the expr id refers to this ref, then set `last` to `v` and return a
205    /// mutable reference to `last`, otherwise return None. This will also
206    /// update `last` if the id matches.
207    pub fn update(&mut self, id: ExprId, v: &Value) -> Option<&mut Value> {
208        if self.id == id {
209            self.last = Some(v.clone());
210            self.last.as_mut()
211        } else {
212            None
213        }
214    }
215}
216
217pub struct TRef<X: GXExt, T: FromValue> {
218    pub r: Ref<X>,
219    pub t: Option<T>,
220}
221
222impl<X: GXExt, T: FromValue> TRef<X, T> {
223    /// Create a new typed reference from `r`
224    ///
225    /// If conversion of `r` fails, return an error.
226    pub fn new(mut r: Ref<X>) -> Result<Self> {
227        let t = r.last.take().map(|v| v.cast_to()).transpose()?;
228        Ok(TRef { r, t })
229    }
230
231    /// Process an update
232    ///
233    /// If the expr id refers to this tref, then convert the value into a `T`
234    /// update `t` and return a mutable reference to the new `T`, otherwise
235    /// return None. Return an Error if the conversion failed.
236    pub fn update(&mut self, id: ExprId, v: &Value) -> Result<Option<&mut T>> {
237        if self.r.id == id {
238            let v = v.clone().cast_to()?;
239            self.t = Some(v);
240            Ok(self.t.as_mut())
241        } else {
242            Ok(None)
243        }
244    }
245}
246
247impl<X: GXExt, T: Into<Value> + FromValue + Clone> TRef<X, T> {
248    /// set the value of the tref `r <-`
249    ///
250    /// This will cause all nodes dependent on this id to update. This is the
251    /// same thing as the `<-` operator in Graphix. This does the same thing as
252    /// `GXHandle::set`
253    pub fn set(&mut self, t: T) -> Result<()> {
254        self.t = Some(t.clone());
255        self.r.set(t)
256    }
257
258    /// set the value pointed to by tref `*r <-`
259    ///
260    /// This will cause all nodes dependent on *id to update. This is the same
261    /// as the `*r <-` operator in Graphix. This does the same thing as
262    /// `GXHandle::set` using the target id.
263    pub fn set_deref(&mut self, t: T) -> Result<()> {
264        self.t = Some(t.clone());
265        self.r.set_deref(t.into())
266    }
267}
268
269atomic_id!(CallableId);
270
271pub struct Callable<X: GXExt> {
272    rt: GXHandle<X>,
273    id: CallableId,
274    env: Env,
275    pub typ: FnType,
276    pub expr: ExprId,
277}
278
279impl<X: GXExt> Drop for Callable<X> {
280    fn drop(&mut self) {
281        let _ = self.rt.0.tx.send(ToGX::DeleteCallable { id: self.id });
282    }
283}
284
285impl<X: GXExt> Callable<X> {
286    /// Get the id of this callable
287    pub fn id(&self) -> CallableId {
288        self.id
289    }
290
291    /// Call the lambda with args
292    ///
293    /// Argument types and arity will be checked and an error will be returned
294    /// if they are wrong. If you call the function more than once before it
295    /// returns there is no guarantee that the returns will arrive in the order
296    /// of the calls. There is no guarantee that a function must return.
297    pub async fn call(&self, args: ValArray) -> Result<()> {
298        if self.typ.args.len() != args.len() {
299            bail!("expected {} args", self.typ.args.len())
300        }
301        for (i, (a, v)) in self.typ.args.iter().zip(args.iter()).enumerate() {
302            if !a.typ.is_a(&self.env, v) {
303                bail!("type mismatch arg {i} expected {}", a.typ)
304            }
305        }
306        self.call_unchecked(args).await
307    }
308
309    /// Call the lambda with args. Argument types and arity will NOT
310    /// be checked. This can result in a runtime panic, invalid
311    /// results, and probably other bad things.
312    pub async fn call_unchecked(&self, args: ValArray) -> Result<()> {
313        self.rt
314            .0
315            .tx
316            .send(ToGX::Call { id: self.id, args })
317            .map_err(|_| anyhow!("runtime is dead"))
318    }
319
320    /// Return Some(v) if this update is the return value of the callable
321    pub fn update<'a>(&self, id: ExprId, v: &'a Value) -> Option<&'a Value> {
322        if self.expr == id {
323            Some(v)
324        } else {
325            None
326        }
327    }
328}
329
330enum DeferredCall {
331    Call(ValArray, oneshot::Sender<Result<()>>),
332    CallUnchecked(ValArray, oneshot::Sender<Result<()>>),
333}
334
335pub struct NamedCallable<X: GXExt> {
336    fname: Ref<X>,
337    current: Option<Callable<X>>,
338    ids: FxHashSet<ExprId>,
339    deferred: Vec<DeferredCall>,
340    h: GXHandle<X>,
341}
342
343impl<X: GXExt> NamedCallable<X> {
344    /// Update the named callable function
345    ///
346    /// This method does two things,
347    /// - Handle late binding. When the name ref updates to an actual function
348    ///   compile the real call site
349    /// - Return Ok(Some(v)) when the called function returns
350    pub async fn update<'a>(
351        &mut self,
352        id: ExprId,
353        v: &'a Value,
354    ) -> Result<Option<&'a Value>> {
355        match self.fname.update(id, v) {
356            Some(v) => {
357                let callable = self.h.compile_callable(v.clone()).await?;
358                self.ids.insert(callable.expr);
359                for dc in self.deferred.drain(..) {
360                    match dc {
361                        DeferredCall::Call(args, reply) => {
362                            let _ = reply.send(callable.call(args).await);
363                        }
364                        DeferredCall::CallUnchecked(args, reply) => {
365                            let _ = reply.send(callable.call_unchecked(args).await);
366                        }
367                    }
368                }
369                self.current = Some(callable);
370                Ok(None)
371            }
372            None if self.ids.contains(&id) => Ok(Some(v)),
373            None => Ok(None),
374        }
375    }
376
377    /// Call the lambda with args
378    ///
379    /// Argument types and arity will be checked and an error will be returned
380    /// if they are wrong. If you call the function more than once before it
381    /// returns there is no guarantee that the returns will arrive in the order
382    /// of the calls. There is no guarantee that a function must return. In
383    /// order to handle late binding you must keep calling `update` while
384    /// waiting for this method.
385    ///
386    /// While a late bound function is unresolved calls will queue internally in
387    /// the NamedCallsite and will happen when the function is resolved.
388    pub async fn call(&mut self, args: ValArray) -> Result<()> {
389        match &self.current {
390            Some(c) => c.call(args).await,
391            None => {
392                let (tx, rx) = oneshot::channel();
393                self.deferred.push(DeferredCall::Call(args, tx));
394                rx.await?
395            }
396        }
397    }
398
399    /// call the function with the specified args
400    ///
401    /// Argument types and arity will NOT be checked by this method. If you call
402    /// the function more than once before it returns there is no guarantee that
403    /// the returns will arrive in the order of the calls. There is no guarantee
404    /// that a function must return. In order to handle late binding you must
405    /// keep calling `update` while waiting for this method.
406    ///
407    /// While a late bound function is unresolved calls will queue internally in
408    /// the NamedCallsite and will happen when the function is resolved.
409    pub async fn call_unchecked(&mut self, args: ValArray) -> Result<()> {
410        match &self.current {
411            Some(c) => c.call(args).await,
412            None => {
413                let (tx, rx) = oneshot::channel();
414                self.deferred.push(DeferredCall::CallUnchecked(args, tx));
415                rx.await?
416            }
417        }
418    }
419}
420
421enum ToGX<X: GXExt> {
422    GetEnv {
423        res: oneshot::Sender<Env>,
424    },
425    Delete {
426        id: ExprId,
427    },
428    Load {
429        path: Source,
430        rt: GXHandle<X>,
431        res: oneshot::Sender<Result<CompRes<X>>>,
432    },
433    Check {
434        path: Source,
435        /// If provided, override the runtime's default resolver chain
436        /// for this check only. Used by IDE tooling that needs
437        /// project-scoped module resolution without rebuilding the
438        /// runtime.
439        resolvers: Option<Vec<ModuleResolver>>,
440        /// If provided, compile the source under this scope rather
441        /// than at the root. Used by IDE tooling editing a graphix
442        /// package crate (`graphix-package-<x>`) so its `mod.gx` body
443        /// registers under `<x>::` rather than at root, matching the
444        /// way the runtime would load it via `mod <x>;` from another
445        /// project. Any pre-existing registrations under that scope
446        /// are scrubbed from the working env first so the package's
447        /// own pre-loaded contents don't trip duplicate-module guards.
448        initial_scope: Option<ArcStr>,
449        res: oneshot::Sender<Result<CheckResult>>,
450    },
451    Compile {
452        text: ArcStr,
453        rt: GXHandle<X>,
454        res: oneshot::Sender<Result<CompRes<X>>>,
455    },
456    CompileCallable {
457        id: Value,
458        rt: GXHandle<X>,
459        res: oneshot::Sender<Result<Callable<X>>>,
460    },
461    CompileRef {
462        id: BindId,
463        rt: GXHandle<X>,
464        res: oneshot::Sender<Result<Ref<X>>>,
465    },
466    Set {
467        id: BindId,
468        v: Value,
469    },
470    Call {
471        id: CallableId,
472        args: ValArray,
473    },
474    DeleteCallable {
475        id: CallableId,
476    },
477}
478
479#[derive(Debug, Clone)]
480pub enum GXEvent {
481    Updated(ExprId, Value),
482    Env(Env),
483}
484
485struct GXHandleInner<X: GXExt> {
486    tx: tmpsc::UnboundedSender<ToGX<X>>,
487    task: JoinHandle<()>,
488    subscriber: netidx::subscriber::Subscriber,
489}
490
491impl<X: GXExt> Drop for GXHandleInner<X> {
492    fn drop(&mut self) {
493        self.task.abort()
494    }
495}
496
497/// A handle to a running GX instance.
498///
499/// Drop the handle to shutdown the associated background tasks.
500pub struct GXHandle<X: GXExt>(Arc<GXHandleInner<X>>);
501
502impl<X: GXExt> fmt::Debug for GXHandle<X> {
503    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
504        write!(f, "GXHandle")
505    }
506}
507
508impl<X: GXExt> Clone for GXHandle<X> {
509    fn clone(&self) -> Self {
510        Self(self.0.clone())
511    }
512}
513
514impl<X: GXExt> GXHandle<X> {
515    /// Get a clone of the netidx subscriber used by this runtime.
516    pub fn subscriber(&self) -> netidx::subscriber::Subscriber {
517        self.0.subscriber.clone()
518    }
519
520    async fn exec<R, F: FnOnce(oneshot::Sender<R>) -> ToGX<X>>(&self, f: F) -> Result<R> {
521        let (tx, rx) = oneshot::channel();
522        self.0.tx.send(f(tx)).map_err(|_| anyhow!("runtime is dead"))?;
523        Ok(rx.await.map_err(|_| anyhow!("runtime did not respond"))?)
524    }
525
526    /// Get a copy of the current graphix environment
527    pub async fn get_env(&self) -> Result<Env> {
528        self.exec(|res| ToGX::GetEnv { res }).await
529    }
530
531    /// Check that a graphix module compiles and type-checks.
532    ///
533    /// If path starts with `netidx:` the module is loaded from
534    /// netidx; otherwise it is loaded from the filesystem (or read
535    /// directly if `Source::Internal`). On success returns a
536    /// `CheckResult` containing both an env snapshot (as it would be
537    /// after the module was compiled) and the set of resolved name
538    /// references the compiler observed — useful for IDE tooling
539    /// (`textDocument/references`). The runtime's live environment
540    /// is not altered — to keep the bindings live, use `compile` or
541    /// `load`.
542    ///
543    /// # Error position info
544    ///
545    /// Compile and parse failures attach a structured context to the
546    /// returned `anyhow::Error` carrying the originating `Origin` and
547    /// `SourcePosition`. IDE tooling and other consumers should
548    /// `downcast_ref` the error rather than scraping the chain's
549    /// message strings:
550    ///
551    /// - [`graphix_compiler::expr::ErrorContext`] — wraps compile-time
552    ///   failures (`bailat!`-style bails and `wrap!`-attached typecheck
553    ///   errors). Carries the failing `Expr`, from which `pos` and
554    ///   `ori` are read.
555    /// - [`graphix_compiler::expr::ParserContext`] — wraps combine
556    ///   parser failures with `Origin` + `SourcePosition` fields.
557    ///
558    /// `anyhow::Error::downcast_ref` walks the context chain via
559    /// anyhow's vtable and returns the outermost match, which for the
560    /// runtime's compile path is the right one.
561    ///
562    /// # IDE / LSP usage
563    ///
564    /// `CheckResult` carries IDE side-channels populated only when
565    /// `env.lsp_mode` is set: `references`, `module_references`,
566    /// `type_references`, `scope_map`, `sig_links`, and
567    /// `module_internals`. The first four record where the compiler saw
568    /// each name and where it resolved; `sig_links` ties `val foo` in a
569    /// `.gxi` to its `let foo = …` impl in the paired `.gx`;
570    /// `module_internals` carries each module's impl-side env so IDE
571    /// queries inside a module body can chase impl bind metadata that
572    /// isn't visible from the project's external view.
573    ///
574    /// To check editor buffers without saving, layer a
575    /// [`ModuleResolver::BufferOverride`] into the resolver chain — its
576    /// override map shadows the on-disk version per path while
577    /// preserving `Source::File` origins, so reference matching and
578    /// goto-def land on the same file paths as a disk check would.
579    pub async fn check(
580        &self,
581        path: Source,
582        initial_scope: Option<ArcStr>,
583    ) -> Result<CheckResult> {
584        Ok(self
585            .exec(|tx| ToGX::Check {
586                path,
587                resolvers: None,
588                initial_scope,
589                res: tx,
590            })
591            .await??)
592    }
593
594    /// Like `check` but overrides the runtime's resolver chain for
595    /// this call only. Used by IDE tooling to compile a project
596    /// against a project-scoped resolver chain (e.g. `Files(<root>)`)
597    /// without having to rebuild the runtime.
598    ///
599    /// `initial_scope`, when set, scopes the entire compilation under
600    /// the given module path (as if the source were the body of a
601    /// `mod <scope> { ... }` block). Used by the LSP when editing a
602    /// graphix package crate so its modules register under the
603    /// package's namespace.
604    pub async fn check_with_resolvers(
605        &self,
606        path: Source,
607        resolvers: Vec<ModuleResolver>,
608        initial_scope: Option<ArcStr>,
609    ) -> Result<CheckResult> {
610        Ok(self
611            .exec(|tx| ToGX::Check {
612                path,
613                resolvers: Some(resolvers),
614                initial_scope,
615                res: tx,
616            })
617            .await??)
618    }
619
620    /// Compile and execute a graphix expression
621    ///
622    /// If it generates results, they will be sent to all the channels that are
623    /// subscribed. When the `CompExp` objects contained in the `CompRes` are
624    /// dropped their corresponding expressions will be deleted. Therefore, you
625    /// can stop execution of the whole expression by dropping the returned
626    /// `CompRes`.
627    pub async fn compile(&self, text: ArcStr) -> Result<CompRes<X>> {
628        Ok(self.exec(|tx| ToGX::Compile { text, res: tx, rt: self.clone() }).await??)
629    }
630
631    /// Load and execute a file or netidx value
632    ///
633    /// When the `CompExp` objects contained in the `CompRes` are
634    /// dropped their corresponding expressions will be
635    /// deleted. Therefore, you can stop execution of the whole file
636    /// by dropping the returned `CompRes`.
637    pub async fn load(&self, path: Source) -> Result<CompRes<X>> {
638        Ok(self.exec(|tx| ToGX::Load { path, res: tx, rt: self.clone() }).await??)
639    }
640
641    /// Compile a callable interface to a lambda id
642    ///
643    /// This is how you call a lambda directly from rust. When the returned
644    /// `Callable` is dropped the associated callsite will be delete.
645    pub async fn compile_callable(&self, id: Value) -> Result<Callable<X>> {
646        Ok(self
647            .exec(|tx| ToGX::CompileCallable { id, rt: self.clone(), res: tx })
648            .await??)
649    }
650
651    /// Compile a callable interface to a late bound function by name
652    ///
653    /// This allows you to call a function by name. Because of late binding it
654    /// has some additional complexity (though less than implementing it
655    /// yourself). You must call `update` on `NamedCallable` when you recieve
656    /// updates from the runtime in order to drive late binding. `update` will
657    /// also return `Some` when one of your function calls returns.
658    pub async fn compile_callable_by_name(
659        &self,
660        env: &Env,
661        scope: &Scope,
662        name: &ModPath,
663    ) -> Result<NamedCallable<X>> {
664        let r = self.compile_ref_by_name(env, scope, name).await?;
665        match &r.typ {
666            Type::Fn(_) => (),
667            t => bail!(
668                "{name} in scope {} has type {t}. expected a function",
669                scope.lexical
670            ),
671        }
672        Ok(NamedCallable {
673            fname: r,
674            current: None,
675            ids: FxHashSet::default(),
676            deferred: vec![],
677            h: self.clone(),
678        })
679    }
680
681    /// Compile a ref to a bind id
682    ///
683    /// This will NOT return an error if the id isn't in the environment.
684    pub async fn compile_ref(&self, id: impl Into<BindId>) -> Result<Ref<X>> {
685        Ok(self
686            .exec(|tx| ToGX::CompileRef { id: id.into(), res: tx, rt: self.clone() })
687            .await??)
688    }
689
690    /// Compile a ref to a name
691    ///
692    /// Return an error if the name does not exist in the environment
693    pub async fn compile_ref_by_name(
694        &self,
695        env: &Env,
696        scope: &Scope,
697        name: &ModPath,
698    ) -> Result<Ref<X>> {
699        let id = env
700            .lookup_bind(&scope.lexical, name)
701            .ok_or_else(|| anyhow!("no such value {name} in scope {}", scope.lexical))?
702            .1
703            .id;
704        self.compile_ref(id).await
705    }
706
707    /// Set the variable idenfified by `id` to `v`
708    ///
709    /// triggering updates of all dependent node trees. This does the same thing
710    /// as`Ref::set` and `TRef::set`
711    pub fn set<T: Into<Value>>(&self, id: BindId, v: T) -> Result<()> {
712        let v = v.into();
713        self.0.tx.send(ToGX::Set { id, v }).map_err(|_| anyhow!("runtime is dead"))
714    }
715
716    /// Call a callable by id with the given arguments
717    ///
718    /// This is a fire-and-forget call that does not wait for the result.
719    /// Unlike `Callable::call`, no type or arity checking is performed.
720    pub fn call(&self, id: CallableId, args: ValArray) -> Result<()> {
721        self.0.tx.send(ToGX::Call { id, args }).map_err(|_| anyhow!("runtime is dead"))
722    }
723}
724
725#[derive(Builder)]
726#[builder(pattern = "owned")]
727pub struct GXConfig<X: GXExt> {
728    /// The subscribe timeout to use when resolving modules in
729    /// netidx. Resolution will fail if the subscription does not
730    /// succeed before this timeout elapses.
731    #[builder(setter(strip_option), default)]
732    resolve_timeout: Option<Duration>,
733    /// The publish timeout to use when sending published batches. Default None.
734    #[builder(setter(strip_option), default)]
735    publish_timeout: Option<Duration>,
736    /// The execution context with any builtins already registered
737    ctx: ExecCtx<GXRt<X>, X::UserEvent>,
738    /// The text of the root module
739    #[builder(setter(strip_option), default)]
740    root: Option<ArcStr>,
741    /// The set of module resolvers to use when resolving loaded modules
742    #[builder(default)]
743    resolvers: Vec<ModuleResolver>,
744    /// The channel that will receive events from the runtime
745    sub: tmpsc::Sender<GPooled<Vec<GXEvent>>>,
746    /// The set of compiler flags. Default empty.
747    #[builder(default)]
748    flags: BitFlags<CFlag>,
749    /// If true, populate IDE side-channels (`ide_binds`,
750    /// references, module references, scope map, type-ref sink) on
751    /// every compile and check. Carries a per-compile cost; only
752    /// the LSP backend should set it.
753    #[builder(default)]
754    lsp_mode: bool,
755}
756
757impl<X: GXExt> GXConfig<X> {
758    /// Create a new config
759    pub fn builder(
760        ctx: ExecCtx<GXRt<X>, X::UserEvent>,
761        sub: tmpsc::Sender<GPooled<Vec<GXEvent>>>,
762    ) -> GXConfigBuilder<X> {
763        GXConfigBuilder::default().ctx(ctx).sub(sub)
764    }
765
766    /// Start the graphix runtime with the specified config,
767    ///
768    /// return a handle capable of interacting with it. root is the text of the
769    /// root module you wish to initially load. This will define the environment
770    /// for the rest of the code compiled by this runtime. The runtime starts
771    /// completely empty, with only the language, no core library, no standard
772    /// library. To build a runtime with the full standard library and nothing
773    /// else simply pass the output of `graphix_stdlib::register` to start.
774    pub async fn start(self) -> Result<GXHandle<X>> {
775        let subscriber = self.ctx.rt.subscriber.clone();
776        let (init_tx, init_rx) = oneshot::channel();
777        let (tx, rx) = tmpsc::unbounded_channel();
778        let task = task::spawn(async move {
779            match GX::new(self).await {
780                Ok(bs) => {
781                    let _ = init_tx.send(Ok(()));
782                    if let Err(e) = bs.run(rx).await {
783                        error!("run loop exited with error {e:?}")
784                    }
785                }
786                Err(e) => {
787                    let _ = init_tx.send(Err(e));
788                }
789            };
790        });
791        init_rx.await??;
792        Ok(GXHandle(Arc::new(GXHandleInner { tx, task, subscriber })))
793    }
794}