Skip to main content

graphix_compiler/
env.rs

1use crate::{
2    expr::{ModPath, Origin, Sandbox},
3    typ::{TVar, Type},
4    BindId, ModuleInternalView, Scope, SigImplLink, TypeRefSite,
5};
6use ahash::{AHashMap, AHashSet};
7use anyhow::{anyhow, bail, Result};
8use arcstr::ArcStr;
9use combine::stream::position::SourcePosition;
10use compact_str::CompactString;
11use immutable_chunkmap::{map::MapS as Map, set::SetS as Set};
12use netidx::path::Path;
13use parking_lot::Mutex;
14use poolshark::{
15    global::{GPooled, Pool},
16    local::LPooled,
17};
18use std::{fmt, iter, mem, ops::Bound, sync::LazyLock};
19use triomphe::Arc;
20
21pub struct Bind {
22    pub id: BindId,
23    pub export: bool,
24    pub typ: Type,
25    pub doc: Option<ArcStr>,
26    pub scope: ModPath,
27    pub name: CompactString,
28    /// Source position where the binding was introduced. Used by IDE
29    /// tooling for go-to-definition; not consulted by the compiler.
30    pub pos: SourcePosition,
31    /// Source origin (file/buffer) where the binding was introduced.
32    pub ori: Arc<Origin>,
33}
34
35impl fmt::Debug for Bind {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        write!(f, "Bind {{ id: {:?}, export: {} }}", self.id, self.export,)
38    }
39}
40
41impl Clone for Bind {
42    fn clone(&self) -> Self {
43        Self {
44            id: self.id,
45            scope: self.scope.clone(),
46            name: self.name.clone(),
47            doc: self.doc.clone(),
48            export: self.export,
49            typ: self.typ.clone(),
50            pos: self.pos,
51            ori: self.ori.clone(),
52        }
53    }
54}
55
56#[derive(Debug, Clone)]
57pub struct TypeDef {
58    pub params: Arc<[(TVar, Option<Type>)]>,
59    pub typ: Type,
60    pub doc: Option<ArcStr>,
61    /// Source position where this typedef was declared. Used by IDE
62    /// tooling for go-to-definition; the compiler doesn't read it.
63    pub pos: SourcePosition,
64    pub ori: Arc<Origin>,
65}
66
67/// IDE side-channels accumulated during compilation when `lsp_mode` is
68/// on. Pushed to from places that hold `&Env` rather than threaded
69/// through every typecheck signature, but tied to a specific `Env` via
70/// the `Arc<Mutex<Lsp>>` field — so two unrelated compiles can't
71/// cross-pollute, and a multi-threaded compile is just a `Mutex` lock
72/// away.
73///
74/// The runtime's `check` installs a fresh `Lsp` at the start of each
75/// check cycle and drains it at the end; non-LSP compiles leave
76/// `Env.lsp` as `None` and pay nothing at the push sites.
77#[derive(Debug)]
78pub struct Lsp {
79    pub type_refs: GPooled<Vec<TypeRefSite>>,
80    pub sig_links: GPooled<Vec<SigImplLink>>,
81    pub module_internals: GPooled<Vec<ModuleInternalView>>,
82}
83
84impl Lsp {
85    /// Fresh, empty sinks pulled from the named pools.
86    pub fn new() -> Self {
87        static TYPE_REF_SITE_POOL: LazyLock<Pool<Vec<TypeRefSite>>> =
88            LazyLock::new(|| Pool::new(64, 65536));
89        static SIG_LINK_POOL: LazyLock<Pool<Vec<SigImplLink>>> =
90            LazyLock::new(|| Pool::new(32, 4096));
91        static MODULE_INTERNAL_VIEW_POOL: LazyLock<Pool<Vec<ModuleInternalView>>> =
92            LazyLock::new(|| Pool::new(32, 4096));
93        Self {
94            type_refs: TYPE_REF_SITE_POOL.take(),
95            sig_links: SIG_LINK_POOL.take(),
96            module_internals: MODULE_INTERNAL_VIEW_POOL.take(),
97        }
98    }
99}
100
101impl Default for Lsp {
102    fn default() -> Self {
103        Self::new()
104    }
105}
106
107#[derive(Clone, Debug, Default)]
108pub struct Env {
109    pub by_id: Map<BindId, Bind>,
110    pub byref_chain: Map<BindId, BindId>,
111    pub binds: Map<ModPath, Map<CompactString, BindId>>,
112    pub used: Map<ModPath, Arc<Vec<ModPath>>>,
113    pub modules: Set<ModPath>,
114    pub typedefs: Map<ModPath, Map<CompactString, TypeDef>>,
115    pub catch: Map<ModPath, BindId>,
116    /// Append-only mirror of every `(scope, name) → BindId` ever
117    /// created via `bind_variable`. Used by IDE tooling for cursor
118    /// → scope completion: it exposes lambda parameters and other
119    /// short-lived bindings that `binds` drops at scope teardown
120    /// and `unbind_variable` removes from `by_id`. Not consulted by
121    /// the compiler. Only populated when `lsp_mode` is set.
122    pub ide_binds: Map<ModPath, Map<CompactString, Bind>>,
123    /// True iff the compiler should populate IDE side-channels
124    /// (`ide_binds`, the `lsp` sinks, etc.). Toggled by the LSP
125    /// runtime; normal compiles leave it unset and pay no IDE cost.
126    pub lsp_mode: bool,
127    /// IDE side-channel sinks for type references, sig→impl links,
128    /// and per-module env snapshots. `Some(_)` only when running
129    /// under an LSP-style check; clones share the inner `Arc<Mutex>`
130    /// so reentrant or concurrent compiles within a single check all
131    /// drain into the same buffer. The runtime swaps this in/out at
132    /// each check boundary.
133    pub lsp: Option<Arc<Mutex<Lsp>>>,
134}
135
136impl Env {
137    pub(super) fn clear(&mut self) {
138        let Self {
139            by_id,
140            binds,
141            byref_chain,
142            used,
143            modules,
144            typedefs,
145            catch,
146            ide_binds,
147            lsp_mode: _,
148            lsp: _,
149        } = self;
150        *by_id = Map::new();
151        *binds = Map::new();
152        *byref_chain = Map::new();
153        *used = Map::new();
154        *modules = Set::new();
155        *typedefs = Map::new();
156        *catch = Map::new();
157        *ide_binds = Map::new();
158    }
159
160    // restore the lexical environment to the state it was in at the
161    // snapshot `other`, but leave the bind and type environment
162    // alone. `ide_binds` is preserved across restoration so IDE
163    // tooling sees lambda parameters / let bindings that were
164    // introduced inside the restored region. The `lsp` sink is
165    // preserved on `self` so any pushes that happened inside the
166    // restored region accumulate alongside the rest of the check.
167    pub(super) fn restore_lexical_env(&self, other: Self) -> Self {
168        Self {
169            binds: other.binds,
170            used: other.used,
171            modules: other.modules,
172            typedefs: other.typedefs,
173            by_id: self.by_id.clone(),
174            catch: self.catch.clone(),
175            byref_chain: self.byref_chain.clone(),
176            ide_binds: self.ide_binds.clone(),
177            lsp_mode: self.lsp_mode,
178            lsp: self.lsp.clone(),
179        }
180    }
181
182    pub(super) fn restore_lexical_env_mut(&self, other: &mut Self) -> Self {
183        Self {
184            binds: mem::take(&mut other.binds),
185            used: mem::take(&mut other.used),
186            modules: mem::take(&mut other.modules),
187            typedefs: mem::take(&mut other.typedefs),
188            by_id: self.by_id.clone(),
189            catch: self.catch.clone(),
190            ide_binds: self.ide_binds.clone(),
191            byref_chain: self.byref_chain.clone(),
192            lsp_mode: self.lsp_mode,
193            lsp: self.lsp.clone(),
194        }
195    }
196
197    /// Push a `TypeRefSite` into the active LSP sink, if any. No-op
198    /// when `self.lsp` is `None` (every non-LSP compile).
199    pub fn push_type_ref(&self, site: TypeRefSite) {
200        if let Some(lsp) = &self.lsp {
201            lsp.lock().type_refs.push(site);
202        }
203    }
204
205    /// Push a `SigImplLink` into the active LSP sink, if any.
206    pub fn push_sig_link(&self, link: SigImplLink) {
207        if let Some(lsp) = &self.lsp {
208            lsp.lock().sig_links.push(link);
209        }
210    }
211
212    /// Push a per-module internal-view snapshot into the active LSP
213    /// sink, if any.
214    pub fn push_module_internal_view(&self, view: ModuleInternalView) {
215        if let Some(lsp) = &self.lsp {
216            lsp.lock().module_internals.push(view);
217        }
218    }
219
220    pub fn apply_sandbox(&self, spec: &Sandbox) -> Result<Self> {
221        fn get_bind_name(n: &ModPath) -> Result<(&str, &str)> {
222            let dir = Path::dirname(&**n).ok_or_else(|| anyhow!("unknown module {n}"))?;
223            let k = Path::basename(&**n).ok_or_else(|| anyhow!("unknown module {n}"))?;
224            Ok((dir, k))
225        }
226        match spec {
227            Sandbox::Unrestricted => Ok(self.clone()),
228            Sandbox::Blacklist(bl) => {
229                let mut t = self.clone();
230                for n in bl.iter() {
231                    if t.modules.remove_cow(n) {
232                        t.binds.remove_cow(n);
233                        t.typedefs.remove_cow(n);
234                    } else {
235                        let (dir, k) = get_bind_name(n)?;
236                        let vals = t.binds.get_mut_cow(dir).ok_or_else(|| {
237                            anyhow!("no value {k} in module {dir} and no module {n}")
238                        })?;
239                        if let None = vals.remove_cow(&CompactString::from(k)) {
240                            bail!("no value {k} in module {dir} and no module {n}")
241                        }
242                    }
243                }
244                Ok(t)
245            }
246            Sandbox::Whitelist(wl) => {
247                let mut t = self.clone();
248                let mut modules = AHashSet::default();
249                let mut names: AHashMap<_, AHashSet<_>> = AHashMap::default();
250                for w in wl.iter() {
251                    if t.modules.contains(w) {
252                        modules.insert(w.clone());
253                    } else {
254                        let (dir, n) = get_bind_name(w)?;
255                        let dir = ModPath(Path::from(ArcStr::from(dir)));
256                        let n = CompactString::from(n);
257                        t.binds.get(&dir).and_then(|v| v.get(&n)).ok_or_else(|| {
258                            anyhow!("no value {n} in module {dir} and no module {w}")
259                        })?;
260                        names.entry(dir).or_default().insert(n);
261                    }
262                }
263                t.typedefs = t.typedefs.update_many(
264                    t.typedefs.into_iter().map(|(k, v)| (k.clone(), v.clone())),
265                    |k, v, _| {
266                        if modules.contains(&k) || names.contains_key(&k) {
267                            Some((k, v))
268                        } else {
269                            None
270                        }
271                    },
272                );
273                t.modules =
274                    t.modules.update_many(t.modules.into_iter().cloned(), |k, _| {
275                        if modules.contains(&k) || names.contains_key(&k) {
276                            Some(k)
277                        } else {
278                            None
279                        }
280                    });
281                t.binds = t.binds.update_many(
282                    t.binds.into_iter().map(|(k, v)| (k.clone(), v.clone())),
283                    |k, v, _| {
284                        if modules.contains(&k) {
285                            Some((k, v))
286                        } else if let Some(names) = names.get(&k) {
287                            let v = v.update_many(
288                                v.into_iter().map(|(k, v)| (k.clone(), v.clone())),
289                                |kn, vn, _| {
290                                    if names.contains(&kn) {
291                                        Some((kn, vn))
292                                    } else {
293                                        None
294                                    }
295                                },
296                            );
297                            Some((k, v))
298                        } else {
299                            None
300                        }
301                    },
302                );
303                Ok(t)
304            }
305        }
306    }
307
308    pub fn find_visible<T, F: FnMut(&str, &str) -> Option<T>>(
309        &self,
310        scope: &ModPath,
311        name: &ModPath,
312        mut f: F,
313    ) -> Option<T> {
314        let mut buf = CompactString::from("");
315        let name_scope = Path::dirname(&**name);
316        let name = Path::basename(&**name).unwrap_or("");
317        for scope in Path::dirnames(&**scope).rev() {
318            let used = self.used.get(scope);
319            let used = iter::once(scope)
320                .chain(used.iter().flat_map(|s| s.iter().map(|p| &***p)));
321            for scope in used {
322                let scope = name_scope
323                    .map(|ns| {
324                        buf.clear();
325                        buf.push_str(scope);
326                        if let Some(Path::SEP) = buf.chars().next_back() {
327                            buf.pop();
328                        }
329                        buf.push_str(ns);
330                        buf.as_str()
331                    })
332                    .unwrap_or(scope);
333                if let Some(res) = f(scope, name) {
334                    return Some(res);
335                }
336            }
337        }
338        None
339    }
340
341    pub fn lookup_bind(
342        &self,
343        scope: &ModPath,
344        name: &ModPath,
345    ) -> Option<(&ModPath, &Bind)> {
346        self.find_visible(scope, name, |scope, name| {
347            self.binds.get_full(scope).and_then(|(scope, vars)| {
348                vars.get(name)
349                    .and_then(|bid| self.by_id.get(bid).map(|bind| (scope, bind)))
350            })
351        })
352    }
353
354    pub fn lookup_typedef(&self, scope: &ModPath, name: &ModPath) -> Option<&TypeDef> {
355        self.find_visible(scope, name, |scope, name| {
356            self.typedefs.get(scope).and_then(|m| m.get(name))
357        })
358    }
359
360    /// lookup the bind id of the nearest catch handler in this scope
361    pub fn lookup_catch(&self, scope: &ModPath) -> Result<BindId> {
362        match Path::dirnames(&scope.0).rev().find_map(|scope| self.catch.get(scope)) {
363            Some(id) => Ok(*id),
364            None => bail!("there is no catch visible in {scope}"),
365        }
366    }
367
368    /// lookup binds in scope that match the specified partial
369    /// name. This is intended to be used for IDEs and interactive
370    /// shells, and is not used by the compiler.
371    pub fn lookup_matching(
372        &self,
373        scope: &ModPath,
374        part: &ModPath,
375    ) -> Vec<(CompactString, BindId)> {
376        let mut res = vec![];
377        self.find_visible(scope, part, |scope, part| {
378            if let Some(vars) = self.binds.get(scope) {
379                let r = vars.range::<str, _>((Bound::Included(part), Bound::Unbounded));
380                for (name, bind) in r {
381                    if name.starts_with(part) {
382                        res.push((name.clone(), *bind));
383                    }
384                }
385            }
386            None::<()>
387        });
388        res
389    }
390
391    /// lookup modules in scope that match the specified partial
392    /// name. This is intended to be used for IDEs and interactive
393    /// shells, and is not used by the compiler.
394    pub fn lookup_matching_modules(
395        &self,
396        scope: &ModPath,
397        part: &ModPath,
398    ) -> Vec<ModPath> {
399        let mut res = vec![];
400        self.find_visible(scope, part, |scope, part| {
401            let p = ModPath(Path::from(ArcStr::from(scope)).append(part));
402            for m in self.modules.range((Bound::Included(p.clone()), Bound::Unbounded)) {
403                if m.0.starts_with(&*p.0) {
404                    if let Some(m) = m.strip_prefix(scope) {
405                        if !m.trim().is_empty() {
406                            res.push(ModPath(Path::from(ArcStr::from(m))));
407                        }
408                    }
409                }
410            }
411            None::<()>
412        });
413        res
414    }
415
416    pub fn canonical_modpath(&self, scope: &ModPath, name: &ModPath) -> Option<ModPath> {
417        self.find_visible(scope, name, |scope, name| {
418            let p = ModPath(Path::from(ArcStr::from(scope)).append(name));
419            if self.modules.contains(&p) {
420                Some(p)
421            } else {
422                None
423            }
424        })
425    }
426
427    pub fn deftype(
428        &mut self,
429        scope: &ModPath,
430        name: &str,
431        params: Arc<[(TVar, Option<Type>)]>,
432        typ: Type,
433        doc: Option<ArcStr>,
434        pos: SourcePosition,
435        ori: Arc<Origin>,
436    ) -> Result<()> {
437        if self.typedefs.get(scope).and_then(|m| m.get(name)).is_some() {
438            bail!("{name} is already defined in scope {scope}")
439        }
440        let mut known: LPooled<AHashMap<ArcStr, TVar>> = LPooled::take();
441        let mut declared: LPooled<AHashSet<ArcStr>> = LPooled::take();
442        for (tv, tc) in params.iter() {
443            Type::TVar(tv.clone()).alias_tvars(&mut known);
444            if let Some(tc) = tc {
445                tc.alias_tvars(&mut known);
446            }
447        }
448        typ.alias_tvars(&mut known);
449        for (tv, _) in params.iter() {
450            if !declared.insert(tv.name.clone()) {
451                bail!("duplicate type variable {tv} in definition of {name}");
452            }
453        }
454        for (_, t) in params.iter() {
455            if let Some(t) = t {
456                t.check_tvars_declared(&mut declared)?;
457            }
458        }
459        for dec in declared.iter() {
460            if !known.contains_key(dec) {
461                bail!("unused type parameter {dec} in definition of {name}")
462            }
463        }
464        if self.lsp_mode {
465            // Capture every type-name occurrence inside the typedef
466            // body for IDE find-references. This catches uses that
467            // never go through `Type::lookup_ref` directly (e.g.
468            // `Foo` inside `type Pair = (Foo, Foo)` — typedef bodies
469            // are stored, not type-checked against anything). Done
470            // before we mutably borrow `self.typedefs` below.
471            typ.record_ide_refs(self, scope);
472        }
473        let defs = self.typedefs.get_or_default_cow(scope.clone());
474        defs.insert_cow(name.into(), TypeDef { params, typ, doc, pos, ori });
475        Ok(())
476    }
477
478    pub fn undeftype(&mut self, scope: &ModPath, name: &str) {
479        if let Some(defs) = self.typedefs.get_mut_cow(scope) {
480            defs.remove_cow(&CompactString::from(name));
481            if defs.len() == 0 {
482                self.typedefs.remove_cow(scope);
483            }
484        }
485    }
486
487    /// Drop everything registered at `scope` or any descendant. Used by
488    /// the LSP when re-typechecking a stdlib (or third-party graphix)
489    /// package crate's own source: the runtime's env was pre-loaded
490    /// with that package at startup, but the live edits need to
491    /// register fresh under the same scope. Without scrubbing first,
492    /// re-registration trips the duplicate-module / duplicate-type
493    /// guards.
494    ///
495    /// Returns the number of (scope, name) entries removed across binds
496    /// and typedefs.
497    pub fn unbind_scope_subtree(&mut self, scope: &ModPath) -> usize {
498        fn is_under(s: &ModPath, prefix: &ModPath) -> bool {
499            // Both come from netidx Path. A scope is under `prefix`
500            // if it equals prefix or starts with `prefix + "/"`.
501            let s: &str = s;
502            let p: &str = prefix;
503            if s == p {
504                return true;
505            }
506            if !s.starts_with(p) {
507                return false;
508            }
509            // Avoid matching e.g. `/tu` as a prefix of `/tui`.
510            s.as_bytes().get(p.len()).copied() == Some(b'/')
511        }
512        let mut removed = 0;
513        let bind_scopes: LPooled<Vec<ModPath>> = (&self.binds)
514            .into_iter()
515            .filter(|(s, _)| is_under(s, scope))
516            .map(|(s, _)| s.clone())
517            .collect();
518        for s in &*bind_scopes {
519            if let Some(defs) = self.binds.get(s) {
520                let ids: Vec<BindId> = defs.into_iter().map(|(_, id)| *id).collect();
521                removed += ids.len();
522                for id in &ids {
523                    self.by_id.remove_cow(id);
524                }
525            }
526            self.binds.remove_cow(s);
527            self.ide_binds.remove_cow(s);
528        }
529        let type_scopes: LPooled<Vec<ModPath>> = (&self.typedefs)
530            .into_iter()
531            .filter(|(s, _)| is_under(s, scope))
532            .map(|(s, _)| s.clone())
533            .collect();
534        for s in &*type_scopes {
535            if let Some(defs) = self.typedefs.get(s) {
536                removed += defs.len();
537            }
538            self.typedefs.remove_cow(s);
539        }
540        let used_scopes: LPooled<Vec<ModPath>> = (&self.used)
541            .into_iter()
542            .filter(|(s, _)| is_under(s, scope))
543            .map(|(s, _)| s.clone())
544            .collect();
545        for s in &*used_scopes {
546            self.used.remove_cow(s);
547        }
548        let mod_scopes: LPooled<Vec<ModPath>> =
549            (&self.modules).into_iter().filter(|s| is_under(s, scope)).cloned().collect();
550        for s in &*mod_scopes {
551            self.modules.remove_cow(s);
552        }
553        let catch_scopes: LPooled<Vec<ModPath>> = (&self.catch)
554            .into_iter()
555            .filter(|(s, _)| is_under(s, scope))
556            .map(|(s, _)| s.clone())
557            .collect();
558        for s in &*catch_scopes {
559            self.catch.remove_cow(s);
560        }
561        removed
562    }
563
564    /// create a new binding. If an existing bind exists in the same
565    /// scope shadow it.
566    pub fn bind_variable(
567        &mut self,
568        scope: &ModPath,
569        name: &str,
570        typ: Type,
571        pos: SourcePosition,
572        ori: Arc<Origin>,
573    ) -> &mut Bind {
574        let binds = self.binds.get_or_default_cow(scope.clone());
575        let mut existing = true;
576        let id = binds.get_or_insert_cow(CompactString::from(name), || {
577            existing = false;
578            BindId::new()
579        });
580        if existing {
581            *id = BindId::new();
582        }
583        let bind = self.by_id.get_or_insert_cow(*id, || Bind {
584            export: true,
585            id: *id,
586            scope: scope.clone(),
587            doc: None,
588            name: CompactString::from(name),
589            typ,
590            pos,
591            ori,
592        });
593        if self.lsp_mode {
594            let ide_clone = bind.clone();
595            let ide_defs = self.ide_binds.get_or_default_cow(scope.clone());
596            ide_defs.insert_cow(CompactString::from(name), ide_clone);
597        }
598        self.by_id.get_mut_cow(id).unwrap()
599    }
600
601    /// make the specified name an alias for `id`
602    pub fn alias_variable(&mut self, scope: &ModPath, name: &str, id: BindId) {
603        let binds = self.binds.get_or_default_cow(scope.clone());
604        binds.insert_cow(CompactString::from(name), id);
605    }
606
607    pub fn unbind_variable(&mut self, id: BindId) {
608        if let Some(b) = self.by_id.remove_cow(&id) {
609            if let Some(binds) = self.binds.get_mut_cow(&b.scope) {
610                binds.remove_cow(&b.name);
611                if binds.len() == 0 {
612                    self.binds.remove_cow(&b.scope);
613                }
614            }
615        }
616    }
617
618    pub fn use_in_scope(&mut self, scope: &Scope, name: &ModPath) -> Result<()> {
619        match self.canonical_modpath(&scope.lexical, name) {
620            None => bail!("use {name}: no such module {name} in scope {}", scope.lexical),
621            Some(_) => {
622                let used = self.used.get_or_default_cow(scope.lexical.clone());
623                Ok(Arc::make_mut(used).push(name.clone()))
624            }
625        }
626    }
627
628    pub fn stop_use_in_scope(&mut self, scope: &Scope, name: &ModPath) {
629        if let Some(used) = self.used.get_mut_cow(&scope.lexical) {
630            Arc::make_mut(used).retain(|n| n != name);
631            if used.is_empty() {
632                self.used.remove_cow(&scope.lexical);
633            }
634        }
635    }
636}