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 graphix_compiler::{
17 env::Env,
18 expr::{ExprId, ModPath, ModuleResolver, Source},
19 typ::{FnType, Type},
20 BindId, CFlag, Event, ExecCtx, NoUserEvent, Scope, UserEvent,
21};
22use log::error;
23use netidx::{
24 protocol::valarray::ValArray,
25 publisher::{Value, WriteRequest},
26 subscriber::{self, SubId},
27};
28use netidx_core::atomic_id;
29use netidx_value::FromValue;
30use nohash::IntSet;
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: IntSet<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 { path, resolvers: None, initial_scope, res: tx })
586 .await??)
587 }
588
589 /// Like `check` but overrides the runtime's resolver chain for
590 /// this call only. Used by IDE tooling to compile a project
591 /// against a project-scoped resolver chain (e.g. `Files(<root>)`)
592 /// without having to rebuild the runtime.
593 ///
594 /// `initial_scope`, when set, scopes the entire compilation under
595 /// the given module path (as if the source were the body of a
596 /// `mod <scope> { ... }` block). Used by the LSP when editing a
597 /// graphix package crate so its modules register under the
598 /// package's namespace.
599 pub async fn check_with_resolvers(
600 &self,
601 path: Source,
602 resolvers: Vec<ModuleResolver>,
603 initial_scope: Option<ArcStr>,
604 ) -> Result<CheckResult> {
605 Ok(self
606 .exec(|tx| ToGX::Check {
607 path,
608 resolvers: Some(resolvers),
609 initial_scope,
610 res: tx,
611 })
612 .await??)
613 }
614
615 /// Compile and execute a graphix expression
616 ///
617 /// If it generates results, they will be sent to all the channels that are
618 /// subscribed. When the `CompExp` objects contained in the `CompRes` are
619 /// dropped their corresponding expressions will be deleted. Therefore, you
620 /// can stop execution of the whole expression by dropping the returned
621 /// `CompRes`.
622 pub async fn compile(&self, text: ArcStr) -> Result<CompRes<X>> {
623 Ok(self.exec(|tx| ToGX::Compile { text, res: tx, rt: self.clone() }).await??)
624 }
625
626 /// Load and execute a file or netidx value
627 ///
628 /// When the `CompExp` objects contained in the `CompRes` are
629 /// dropped their corresponding expressions will be
630 /// deleted. Therefore, you can stop execution of the whole file
631 /// by dropping the returned `CompRes`.
632 pub async fn load(&self, path: Source) -> Result<CompRes<X>> {
633 Ok(self.exec(|tx| ToGX::Load { path, res: tx, rt: self.clone() }).await??)
634 }
635
636 /// Compile a callable interface to a lambda id
637 ///
638 /// This is how you call a lambda directly from rust. When the returned
639 /// `Callable` is dropped the associated callsite will be delete.
640 pub async fn compile_callable(&self, id: Value) -> Result<Callable<X>> {
641 Ok(self
642 .exec(|tx| ToGX::CompileCallable { id, rt: self.clone(), res: tx })
643 .await??)
644 }
645
646 /// Compile a callable interface to a late bound function by name
647 ///
648 /// This allows you to call a function by name. Because of late binding it
649 /// has some additional complexity (though less than implementing it
650 /// yourself). You must call `update` on `NamedCallable` when you recieve
651 /// updates from the runtime in order to drive late binding. `update` will
652 /// also return `Some` when one of your function calls returns.
653 pub async fn compile_callable_by_name(
654 &self,
655 env: &Env,
656 scope: &Scope,
657 name: &ModPath,
658 ) -> Result<NamedCallable<X>> {
659 let r = self.compile_ref_by_name(env, scope, name).await?;
660 match &r.typ {
661 Type::Fn(_) => (),
662 t => bail!(
663 "{name} in scope {} has type {t}. expected a function",
664 scope.lexical
665 ),
666 }
667 Ok(NamedCallable {
668 fname: r,
669 current: None,
670 ids: IntSet::default(),
671 deferred: vec![],
672 h: self.clone(),
673 })
674 }
675
676 /// Compile a ref to a bind id
677 ///
678 /// This will NOT return an error if the id isn't in the environment.
679 pub async fn compile_ref(&self, id: impl Into<BindId>) -> Result<Ref<X>> {
680 Ok(self
681 .exec(|tx| ToGX::CompileRef { id: id.into(), res: tx, rt: self.clone() })
682 .await??)
683 }
684
685 /// Compile a ref to a name
686 ///
687 /// Return an error if the name does not exist in the environment
688 pub async fn compile_ref_by_name(
689 &self,
690 env: &Env,
691 scope: &Scope,
692 name: &ModPath,
693 ) -> Result<Ref<X>> {
694 let id = env
695 .lookup_bind(&scope.lexical, name)
696 .ok_or_else(|| anyhow!("no such value {name} in scope {}", scope.lexical))?
697 .1
698 .id;
699 self.compile_ref(id).await
700 }
701
702 /// Set the variable idenfified by `id` to `v`
703 ///
704 /// triggering updates of all dependent node trees. This does the same thing
705 /// as`Ref::set` and `TRef::set`
706 pub fn set<T: Into<Value>>(&self, id: BindId, v: T) -> Result<()> {
707 let v = v.into();
708 self.0.tx.send(ToGX::Set { id, v }).map_err(|_| anyhow!("runtime is dead"))
709 }
710
711 /// Call a callable by id with the given arguments
712 ///
713 /// This is a fire-and-forget call that does not wait for the result.
714 /// Unlike `Callable::call`, no type or arity checking is performed.
715 pub fn call(&self, id: CallableId, args: ValArray) -> Result<()> {
716 self.0.tx.send(ToGX::Call { id, args }).map_err(|_| anyhow!("runtime is dead"))
717 }
718}
719
720#[derive(Builder)]
721#[builder(pattern = "owned")]
722pub struct GXConfig<X: GXExt> {
723 /// The subscribe timeout to use when resolving modules in
724 /// netidx. Resolution will fail if the subscription does not
725 /// succeed before this timeout elapses.
726 #[builder(setter(strip_option), default)]
727 resolve_timeout: Option<Duration>,
728 /// The publish timeout to use when sending published batches. Default None.
729 #[builder(setter(strip_option), default)]
730 publish_timeout: Option<Duration>,
731 /// The execution context with any builtins already registered
732 ctx: ExecCtx<GXRt<X>, X::UserEvent>,
733 /// The text of the root module
734 #[builder(setter(strip_option), default)]
735 root: Option<ArcStr>,
736 /// The set of module resolvers to use when resolving loaded modules
737 #[builder(default)]
738 resolvers: Vec<ModuleResolver>,
739 /// The channel that will receive events from the runtime
740 sub: tmpsc::Sender<GPooled<Vec<GXEvent>>>,
741 /// The set of compiler flags. Default empty.
742 #[builder(default)]
743 flags: BitFlags<CFlag>,
744 /// If true, populate IDE side-channels (`ide_binds`,
745 /// references, module references, scope map, type-ref sink) on
746 /// every compile and check. Carries a per-compile cost; only
747 /// the LSP backend should set it.
748 #[builder(default)]
749 lsp_mode: bool,
750}
751
752impl<X: GXExt> GXConfig<X> {
753 /// Create a new config
754 pub fn builder(
755 ctx: ExecCtx<GXRt<X>, X::UserEvent>,
756 sub: tmpsc::Sender<GPooled<Vec<GXEvent>>>,
757 ) -> GXConfigBuilder<X> {
758 GXConfigBuilder::default().ctx(ctx).sub(sub)
759 }
760
761 /// Start the graphix runtime with the specified config,
762 ///
763 /// return a handle capable of interacting with it. root is the text of the
764 /// root module you wish to initially load. This will define the environment
765 /// for the rest of the code compiled by this runtime. The runtime starts
766 /// completely empty, with only the language, no core library, no standard
767 /// library. To build a runtime with the full standard library and nothing
768 /// else simply pass the output of `graphix_stdlib::register` to start.
769 pub async fn start(self) -> Result<GXHandle<X>> {
770 let subscriber = self.ctx.rt.subscriber.clone();
771 let (init_tx, init_rx) = oneshot::channel();
772 let (tx, rx) = tmpsc::unbounded_channel();
773 let task = task::spawn(async move {
774 match GX::new(self).await {
775 Ok(bs) => {
776 let _ = init_tx.send(Ok(()));
777 if let Err(e) = bs.run(rx).await {
778 error!("run loop exited with error {e:?}")
779 }
780 }
781 Err(e) => {
782 let _ = init_tx.send(Err(e));
783 }
784 };
785 });
786 init_rx.await??;
787 Ok(GXHandle(Arc::new(GXHandleInner { tx, task, subscriber })))
788 }
789}