egglog/
typechecking.rs

1use std::hash::Hasher;
2
3use crate::Context;
4use crate::proofs::proof_container_rebuild::register_container_rebuild_from_spec;
5use crate::{
6    core::{CoreActionContext, CoreRule, GenericActionsExt, ResolvedCall},
7    *,
8};
9use ast::{
10    MappedExprExt, ResolvedAction, ResolvedExpr, ResolvedFact, ResolvedRule, ResolvedVar, Rule,
11    RuleEvalMode,
12};
13use core_relations::ExternalFunction;
14use egglog_ast::generic_ast::GenericAction;
15use egglog_bridge::ActionRegistry;
16use enum_map::EnumMap;
17use std::sync::{Arc, RwLock};
18
19// `ExternalFunction` wrapper for `PurePrim`. Holds the primitive
20// directly so the dispatch chain `external_funcs[id].invoke(...)` →
21// `T::apply(...)` is just one vtable hop plus a direct call — no
22// closure indirection that defeats inlining.
23#[derive(Clone)]
24struct PurePrimWrapper<T> {
25    prim: T,
26    /// The call-site [`Context`] this wrapper stamps onto the
27    /// `PureState` before dispatching. `register_per_context` commits
28    /// one wrapper per valid `Context` for the trait, so the
29    /// typechecker's pick at each call site is encoded directly here.
30    ctx: Context,
31}
32
33impl<T: PurePrim + Clone> ExternalFunction for PurePrimWrapper<T> {
34    fn invoke(&self, exec_state: &mut ExecutionState, args: &[Value]) -> Option<Value> {
35        self.prim.apply(PureState::wrap(exec_state, self.ctx), args)
36    }
37}
38
39// `ExternalFunction` wrapper for primitives that need the
40// `ActionRegistry` (`ReadPrim`, `WritePrim`, `FullPrim`). One generic
41// over the `Wrap` strategy that knows how to construct the right
42// state type and dispatch to the primitive's `apply`.
43#[derive(Clone)]
44struct RegistryPrimWrapper<T, S> {
45    prim: T,
46    registry: Arc<RwLock<ActionRegistry>>,
47    /// Stamped onto the state wrapper.
48    ctx: Context,
49    _wrap: std::marker::PhantomData<fn() -> S>,
50}
51
52trait RegistryWrap<T>: Clone + Send + Sync {
53    fn invoke(
54        prim: &T,
55        exec_state: &mut ExecutionState,
56        ctx: Context,
57        args: &[Value],
58        registry: &ActionRegistry,
59    ) -> Option<Value>;
60}
61
62#[derive(Clone)]
63struct WrapRead;
64impl<T: ReadPrim> RegistryWrap<T> for WrapRead {
65    #[inline]
66    fn invoke(
67        prim: &T,
68        exec_state: &mut ExecutionState,
69        ctx: Context,
70        args: &[Value],
71        registry: &ActionRegistry,
72    ) -> Option<Value> {
73        prim.apply(ReadState::wrap(exec_state, registry, ctx), args)
74    }
75}
76#[derive(Clone)]
77struct WrapWrite;
78impl<T: WritePrim> RegistryWrap<T> for WrapWrite {
79    #[inline]
80    fn invoke(
81        prim: &T,
82        exec_state: &mut ExecutionState,
83        ctx: Context,
84        args: &[Value],
85        registry: &ActionRegistry,
86    ) -> Option<Value> {
87        prim.apply(WriteState::wrap(exec_state, registry, ctx), args)
88    }
89}
90#[derive(Clone)]
91struct WrapFull;
92impl<T: FullPrim> RegistryWrap<T> for WrapFull {
93    #[inline]
94    fn invoke(
95        prim: &T,
96        exec_state: &mut ExecutionState,
97        ctx: Context,
98        args: &[Value],
99        registry: &ActionRegistry,
100    ) -> Option<Value> {
101        prim.apply(FullState::wrap(exec_state, registry, ctx), args)
102    }
103}
104
105impl<T: Clone + Send + Sync + 'static, S: RegistryWrap<T> + 'static> ExternalFunction
106    for RegistryPrimWrapper<T, S>
107{
108    fn invoke(&self, exec_state: &mut ExecutionState, args: &[Value]) -> Option<Value> {
109        let registry = self.registry.read().unwrap();
110        S::invoke(&self.prim, exec_state, self.ctx, args, &registry)
111    }
112}
113
114#[derive(Clone, Debug)]
115pub struct FuncType {
116    pub name: String,
117    pub subtype: FunctionSubtype,
118    pub input: Vec<ArcSort>,
119    pub output: ArcSort,
120}
121
122impl PartialEq for FuncType {
123    fn eq(&self, other: &Self) -> bool {
124        if self.name == other.name
125            && self.subtype == other.subtype
126            && self.output.name() == other.output.name()
127        {
128            if self.input.len() != other.input.len() {
129                return false;
130            }
131            for (a, b) in self.input.iter().zip(other.input.iter()) {
132                if a.name() != b.name() {
133                    return false;
134                }
135            }
136            true
137        } else {
138            false
139        }
140    }
141}
142
143impl Eq for FuncType {}
144
145impl Hash for FuncType {
146    fn hash<H: Hasher>(&self, state: &mut H) {
147        self.name.hash(state);
148        self.subtype.hash(state);
149        self.output.name().hash(state);
150        for inp in &self.input {
151            inp.name().hash(state);
152        }
153    }
154}
155/// Validators take a termdag and arguments (as TermIds) and return
156/// a newly computed TermId if the primitive application is valid,
157/// or None if it is invalid.
158pub type PrimitiveValidator = Arc<dyn Fn(&mut TermDag, &[TermId]) -> Option<TermId> + Send + Sync>;
159
160#[derive(Clone)]
161pub struct PrimitiveWithId {
162    pub(crate) primitive: Arc<dyn Primitive>,
163    pub(crate) validator: Option<PrimitiveValidator>,
164    /// Runtime entrypoints for the contexts this primitive is valid in.
165    /// The primitive definition is stored once, while each context keeps
166    /// its own backend id so higher-order dispatch can still recover the
167    /// application context at runtime.
168    pub(crate) context_ids: EnumMap<Context, Option<ExternalFunctionId>>,
169}
170
171impl PrimitiveWithId {
172    /// Takes the full signature of a primitive (both input and output types).
173    /// Returns whether the primitive is compatible with this signature.
174    pub fn accept(&self, tys: &[Arc<dyn Sort>], typeinfo: &TypeInfo) -> bool {
175        let mut constraints = vec![];
176        let lits: Vec<_> = (0..tys.len())
177            .map(|i| AtomTerm::Literal(Span::Panic, Literal::Int(i as i64)))
178            .collect();
179        for (lit, ty) in lits.iter().zip(tys.iter()) {
180            constraints.push(constraint::assign(lit.clone(), ty.clone()))
181        }
182        constraints.extend(
183            self.primitive
184                .get_type_constraints(&Span::Panic)
185                .get(&lits, typeinfo),
186        );
187        let problem = Problem {
188            constraints,
189            range: HashSet::default(),
190        };
191        problem.solve(|sort| sort.name()).is_ok()
192    }
193
194    /// Returns whether this primitive has a runtime entrypoint for `context`.
195    pub fn is_valid_in_context(&self, context: Context) -> bool {
196        self.context_ids[context].is_some()
197    }
198}
199
200impl Debug for PrimitiveWithId {
201    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202        write!(f, "Prim({})", self.primitive.name())
203    }
204}
205
206/// Stores resolved typechecking information.
207#[derive(Clone, Default)]
208pub struct TypeInfo {
209    mksorts: HashMap<String, MkSort>,
210    // TODO(yz): I want to get rid of this as now we have user-defined primitives and constraint based type checking
211    reserved_primitives: HashSet<&'static str>,
212    pub(crate) sorts: HashMap<String, Arc<dyn Sort>>,
213    primitives: HashMap<String, Vec<PrimitiveWithId>>,
214    func_types: HashMap<String, FuncType>,
215    pub(crate) global_sorts: HashMap<String, ArcSort>,
216    /// Sorts that do not allow union (e.g., from `:no-union` sorts or relations).
217    pub(crate) non_unionable_sorts: HashSet<String>,
218}
219
220// These methods need to be on the `EGraph` in order to
221// register sorts and primitives with the backend.
222impl EGraph {
223    /// Add a user-defined sort to the e-graph.
224    ///
225    /// Also look at [`prelude::add_base_sort`] for a convenience method for adding user-defined sorts
226    pub fn add_sort<S: Sort + 'static>(&mut self, sort: S, span: Span) -> Result<(), TypeError> {
227        self.add_arcsort(Arc::new(sort), span)
228    }
229
230    /// Declare a sort. This corresponds to the `sort` keyword in egglog.
231    /// It can either declares a new [`EqSort`] if `presort_and_args` is not provided,
232    /// or an instantiation of a presort (e.g., containers like `Vec`).
233    pub fn declare_sort(
234        &mut self,
235        name: impl Into<String>,
236        presort_and_args: &Option<(String, Vec<Expr>)>,
237        span: Span,
238    ) -> Result<(), TypeError> {
239        let name = name.into();
240        if self.type_info.func_types.contains_key(&name) {
241            return Err(TypeError::FunctionAlreadyBound(name, span));
242        }
243
244        let sort = match presort_and_args {
245            None => Arc::new(EqSort { name }),
246            Some((presort, args)) => {
247                if let Some(mksort) = self.type_info.mksorts.get(presort) {
248                    mksort(&mut self.type_info, name, args)?
249                } else {
250                    return Err(TypeError::PresortNotFound(presort.clone(), span));
251                }
252            }
253        };
254
255        self.add_arcsort(sort, span)
256    }
257
258    /// Add a user-defined sort to the e-graph.
259    pub fn add_arcsort(&mut self, sort: ArcSort, span: Span) -> Result<(), TypeError> {
260        sort.register_type(&mut self.backend);
261
262        let name = sort.name();
263        match self.type_info.sorts.entry(name.to_owned()) {
264            HEntry::Occupied(_) => Err(TypeError::SortAlreadyBound(name.to_owned(), span)),
265            HEntry::Vacant(e) => {
266                e.insert(sort.clone());
267                sort.register_primitives(self);
268                Ok(())
269            }
270        }
271    }
272
273    /// Register a [`PurePrim`]. Pass `None` for the validator if not
274    /// using the proof checker.
275    ///
276    /// Pick the trait whose state wrapper matches the body's needs:
277    /// [`PurePrim`] for pure ops, [`WritePrim`] for writes,
278    /// [`ReadPrim`] for table reads, [`FullPrim`] for both. The Rust
279    /// type checker enforces the body only uses methods the chosen
280    /// state allows.
281    pub fn add_pure_primitive<T>(&mut self, x: T, validator: Option<PrimitiveValidator>)
282    where
283        T: PurePrim + Clone,
284    {
285        self.register_per_context(x, validator, PureState::valid_contexts(), |x, ctx| {
286            Box::new(PurePrimWrapper { prim: x, ctx })
287        });
288    }
289
290    /// Register a [`WritePrim`]. Pass `None` for the validator if not
291    /// using the proof checker.
292    pub fn add_write_primitive<T>(&mut self, x: T, validator: Option<PrimitiveValidator>)
293    where
294        T: WritePrim + Clone,
295    {
296        self.register_registry_primitive::<T, WrapWrite>(
297            x,
298            validator,
299            WriteState::valid_contexts(),
300        );
301    }
302
303    /// Register a [`ReadPrim`]. Pass `None` for the validator if not
304    /// using the proof checker.
305    pub fn add_read_primitive<T>(&mut self, x: T, validator: Option<PrimitiveValidator>)
306    where
307        T: ReadPrim + Clone,
308    {
309        self.register_registry_primitive::<T, WrapRead>(x, validator, ReadState::valid_contexts());
310    }
311
312    /// Register a [`FullPrim`]. Pass `None` for the validator if not
313    /// using the proof checker.
314    pub fn add_full_primitive<T>(&mut self, x: T, validator: Option<PrimitiveValidator>)
315    where
316        T: FullPrim + Clone,
317    {
318        self.register_registry_primitive::<T, WrapFull>(x, validator, FullState::valid_contexts());
319    }
320
321    fn register_registry_primitive<T, S>(
322        &mut self,
323        x: T,
324        validator: Option<PrimitiveValidator>,
325        valid_ctxs: &[Context],
326    ) where
327        T: Primitive + Clone,
328        S: RegistryWrap<T> + 'static,
329    {
330        let registry = self.backend.action_registry().clone();
331        self.register_per_context(x, validator, valid_ctxs, move |x, ctx| {
332            Box::new(RegistryPrimWrapper::<T, S> {
333                prim: x,
334                registry: registry.clone(),
335                ctx,
336                _wrap: std::marker::PhantomData,
337            })
338        });
339    }
340
341    /// Shared registration engine. Stores one primitive definition, plus
342    /// one runtime id per valid [`Context`]. Each wrapper carries its
343    /// specific context stamped onto the state wrapper at invoke time.
344    ///
345    /// The typechecker filters by the context-id mask at each call site;
346    /// an `unstable-fn` value built around the primitive bakes *all*
347    /// signature-matching context ids, and `FunctionContainer::apply`
348    /// picks the one whose context matches the application ctx — so
349    /// values flow freely across contexts.
350    fn register_per_context<T, F>(
351        &mut self,
352        x: T,
353        validator: Option<PrimitiveValidator>,
354        valid_ctxs: &[Context],
355        mut build_wrapper: F,
356    ) where
357        T: Primitive + Clone,
358        F: FnMut(T, Context) -> Box<dyn ExternalFunction>,
359    {
360        let primitive: Arc<dyn Primitive> = Arc::new(x.clone());
361        let name = primitive.name().to_owned();
362        let context_ids = EnumMap::from_fn(|ctx| {
363            valid_ctxs.contains(&ctx).then(|| {
364                self.backend
365                    .register_external_func(build_wrapper(x.clone(), ctx))
366            })
367        });
368        self.type_info
369            .primitives
370            .entry(name)
371            .or_default()
372            .push(PrimitiveWithId {
373                primitive,
374                validator,
375                context_ids,
376            });
377    }
378}
379
380impl EGraph {
381    pub(crate) fn typecheck_program(
382        &mut self,
383        program: &Vec<NCommand>,
384    ) -> Result<Vec<ResolvedNCommand>, TypeError> {
385        let mut result = vec![];
386        for command in program {
387            result.push(self.typecheck_command(command)?);
388        }
389        Ok(result)
390    }
391
392    fn typecheck_command(&mut self, command: &NCommand) -> Result<ResolvedNCommand, TypeError> {
393        let symbol_gen = &mut self.parser.symbol_gen;
394
395        let command: ResolvedNCommand = match command {
396            NCommand::Function(fdecl) => {
397                let resolved = self.type_info.typecheck_function(symbol_gen, fdecl)?;
398                // If this is a let binding, add it to global_sorts
399                // This preserves bahavior for lets after desugaring
400                if resolved.internal_let {
401                    let output_sort = self.type_info.sorts.get(&fdecl.schema.output).unwrap();
402                    self.type_info
403                        .global_sorts
404                        .insert(fdecl.name.clone(), output_sort.clone());
405                }
406                ResolvedNCommand::Function(resolved)
407            }
408            NCommand::NormRule { rule } => ResolvedNCommand::NormRule {
409                rule: self
410                    .type_info
411                    .typecheck_rule(symbol_gen, rule, self.seminaive)?,
412            },
413            NCommand::Sort {
414                span,
415                name,
416                presort_and_args,
417                uf,
418                proof_func,
419                container_rebuild,
420                proof_constructors,
421                unionable,
422            } => {
423                // Note this is bad since typechecking should be pure and idempotent
424                // Otherwise typechecking the same program twice will fail
425                self.declare_sort(name.clone(), presort_and_args, span.clone())?;
426                // Mark as non-unionable if the sort declaration says so
427                if !unionable {
428                    self.type_info.non_unionable_sorts.insert(name.clone());
429                }
430                // Record this sort's UF / proof tables in proof_state (as
431                // run_command also does) so the container rebuild registration
432                // below can recover them — including this container's own proof
433                // table, which has not run yet.
434                if let Some((uf_ctor, uf_index)) = uf {
435                    self.proof_state
436                        .uf_parent
437                        .insert(name.clone(), uf_ctor.clone());
438                    if let Some(uf_index) = uf_index {
439                        self.proof_state
440                            .uf_function
441                            .insert(name.clone(), uf_index.clone());
442                    }
443                }
444                if let Some(pf) = proof_func {
445                    self.proof_state
446                        .proof_func_parent
447                        .insert(name.clone(), pf.clone());
448                }
449                // The Proof sort records the global proof constructors; restore
450                // them into proof_state so container rebuild can recover them
451                // (the `Proof` datatype name is this sort's own name).
452                if let Some(pc) = proof_constructors {
453                    let names = &mut self.proof_state.proof_names;
454                    names.proof_datatype = name.clone();
455                    names.congr_constructor = pc.congr.clone();
456                    names.eq_trans_constructor = pc.trans.clone();
457                    names.eq_sym_constructor = pc.sym.clone();
458                    names.container_normalize_constructor = pc.normalize.clone();
459                }
460                // A container sort under the term/proof encoding carries a spec
461                // for its rebuild primitives; register them here so they are
462                // available both during encoding and when the desugared program
463                // is re-parsed.
464                if let Some(spec) = container_rebuild {
465                    register_container_rebuild_from_spec(self, name, spec);
466                }
467                ResolvedNCommand::Sort {
468                    span: span.clone(),
469                    name: name.clone(),
470                    presort_and_args: presort_and_args.clone(),
471                    uf: uf.clone(),
472                    proof_func: proof_func.clone(),
473                    container_rebuild: container_rebuild.clone(),
474                    proof_constructors: proof_constructors.clone(),
475                    unionable: *unionable,
476                }
477            }
478            NCommand::CoreAction(action @ Action::Let(span, var, _)) => {
479                let action = self.type_info.typecheck_standalone_action(
480                    symbol_gen,
481                    action,
482                    &Default::default(),
483                    Context::Full,
484                )?;
485                self.ensure_global_name_prefix(span, var)?;
486                let ResolvedAction::Let(_, resolved_var, _) = &action else {
487                    unreachable!("typechecking an Action::Let should return ResolvedAction::Let")
488                };
489                self.type_info
490                    .global_sorts
491                    .insert(resolved_var.name.clone(), resolved_var.sort.clone());
492                ResolvedNCommand::CoreAction(action)
493            }
494            NCommand::CoreAction(action) => {
495                ResolvedNCommand::CoreAction(self.type_info.typecheck_standalone_action(
496                    symbol_gen,
497                    action,
498                    &Default::default(),
499                    Context::Full,
500                )?)
501            }
502            NCommand::Extract(span, expr, variants) => {
503                let res_expr = self.type_info.typecheck_standalone_expr(
504                    symbol_gen,
505                    expr,
506                    &Default::default(),
507                    Context::Full,
508                )?;
509
510                let res_variants = self.type_info.typecheck_standalone_expr(
511                    symbol_gen,
512                    variants,
513                    &Default::default(),
514                    Context::Full,
515                )?;
516                if res_variants.output_type().name() != I64Sort.name() {
517                    return Err(TypeError::Mismatch {
518                        expr: variants.clone(),
519                        expected: I64Sort.to_arcsort(),
520                        actual: res_variants.output_type(),
521                    });
522                }
523
524                ResolvedNCommand::Extract(span.clone(), res_expr, res_variants)
525            }
526            NCommand::Check(span, facts) => ResolvedNCommand::Check(
527                span.clone(),
528                self.type_info.typecheck_facts(symbol_gen, facts)?,
529            ),
530            NCommand::Fail(span, cmd) => {
531                ResolvedNCommand::Fail(span.clone(), Box::new(self.typecheck_command(cmd)?))
532            }
533            NCommand::RunSchedule(schedule) => ResolvedNCommand::RunSchedule(
534                self.type_info.typecheck_schedule(symbol_gen, schedule)?,
535            ),
536            NCommand::Pop(span, n) => ResolvedNCommand::Pop(span.clone(), *n),
537            NCommand::Push(n) => ResolvedNCommand::Push(*n),
538            NCommand::AddRuleset(span, ruleset) => {
539                ResolvedNCommand::AddRuleset(span.clone(), ruleset.clone())
540            }
541            NCommand::UnstableCombinedRuleset(span, name, sub_rulesets) => {
542                ResolvedNCommand::UnstableCombinedRuleset(
543                    span.clone(),
544                    name.clone(),
545                    sub_rulesets.clone(),
546                )
547            }
548            NCommand::PrintOverallStatistics(span, file) => {
549                ResolvedNCommand::PrintOverallStatistics(span.clone(), file.clone())
550            }
551            NCommand::PrintFunction(span, table, size, file, mode) => {
552                ResolvedNCommand::PrintFunction(
553                    span.clone(),
554                    table.clone(),
555                    *size,
556                    file.clone(),
557                    *mode,
558                )
559            }
560            NCommand::PrintSize(span, n) => {
561                // Should probably also resolve the function symbol here
562                ResolvedNCommand::PrintSize(span.clone(), n.clone())
563            }
564            NCommand::ProveExists(span, constructor) => {
565                let func_type = self
566                    .type_info
567                    .get_func_type(constructor)
568                    .ok_or_else(|| TypeError::UnboundFunction(constructor.clone(), span.clone()))?;
569                if func_type.subtype != FunctionSubtype::Constructor {
570                    return Err(TypeError::ProveExistsRequiresConstructor(
571                        constructor.clone(),
572                        span.clone(),
573                    ));
574                }
575                ResolvedNCommand::ProveExists(span.clone(), ResolvedCall::Func(func_type.clone()))
576            }
577            NCommand::Output { span, file, exprs } => {
578                let exprs = exprs
579                    .iter()
580                    .map(|expr| {
581                        self.type_info.typecheck_standalone_expr(
582                            symbol_gen,
583                            expr,
584                            &Default::default(),
585                            Context::Full,
586                        )
587                    })
588                    .collect::<Result<Vec<_>, _>>()?;
589                ResolvedNCommand::Output {
590                    span: span.clone(),
591                    file: file.clone(),
592                    exprs,
593                }
594            }
595            NCommand::Input { span, name, file } => ResolvedNCommand::Input {
596                span: span.clone(),
597                name: name.clone(),
598                file: file.clone(),
599            },
600            NCommand::UserDefined(span, name, exprs) => {
601                ResolvedNCommand::UserDefined(span.clone(), name.clone(), exprs.clone())
602            }
603        };
604        if let ResolvedNCommand::NormRule { rule } = &command {
605            self.warn_for_prefixed_non_globals_in_rule(rule)?;
606        }
607        Ok(command)
608    }
609
610    fn warn_for_prefixed_non_globals_in_var(
611        &mut self,
612        span: &Span,
613        var: &ResolvedVar,
614    ) -> Result<(), TypeError> {
615        if var.is_global_ref {
616            return Ok(());
617        }
618        if var.name.starts_with(crate::GLOBAL_NAME_PREFIX) {
619            self.warn_prefixed_non_globals(span, &var.name)?;
620        }
621        Ok(())
622    }
623
624    fn warn_for_prefixed_non_globals_in_rule(
625        &mut self,
626        rule: &ResolvedRule,
627    ) -> Result<(), TypeError> {
628        let mut res: Result<(), TypeError> = Ok(());
629
630        for fact in &rule.body {
631            fact.visit_vars(&mut |span, var| {
632                if res.is_ok() {
633                    res = self.warn_for_prefixed_non_globals_in_var(span, var);
634                }
635            });
636        }
637
638        rule.head.visit_vars(&mut |span, var| {
639            if res.is_ok() {
640                res = self.warn_for_prefixed_non_globals_in_var(span, var);
641            }
642        });
643        res
644    }
645}
646
647impl TypeInfo {
648    /// Adds a sort constructor to the typechecker's known set of types.
649    pub fn add_presort<S: Presort>(&mut self, span: Span) -> Result<(), TypeError> {
650        let name = S::presort_name();
651        match self.mksorts.entry(name.to_owned()) {
652            HEntry::Occupied(_) => Err(TypeError::SortAlreadyBound(name.to_owned(), span)),
653            HEntry::Vacant(e) => {
654                e.insert(S::make_sort);
655                self.reserved_primitives.extend(S::reserved_primitives());
656                Ok(())
657            }
658        }
659    }
660
661    /// Returns all sorts that satisfy the type and predicate.
662    pub fn get_sorts_by<S: Sort>(&self, pred: impl Fn(&Arc<S>) -> bool) -> Vec<Arc<S>> {
663        let mut results = Vec::new();
664        for sort in self.sorts.values() {
665            let sort = sort.clone().as_arc_any();
666            if let Ok(sort) = Arc::downcast(sort)
667                && pred(&sort)
668            {
669                results.push(sort);
670            }
671        }
672        results
673    }
674
675    /// Returns all sorts based on the type.
676    pub fn get_sorts<S: Sort>(&self) -> Vec<Arc<S>> {
677        self.get_sorts_by(|_| true)
678    }
679
680    /// Returns a sort that satisfies the type and predicate.
681    pub fn get_sort_by<S: Sort>(&self, pred: impl Fn(&Arc<S>) -> bool) -> Arc<S> {
682        let results = self.get_sorts_by(pred);
683        assert_eq!(
684            results.len(),
685            1,
686            "Expected exactly one sort for type {}",
687            std::any::type_name::<S>()
688        );
689        results.into_iter().next().unwrap()
690    }
691
692    /// Returns a sort based on the type.
693    pub fn get_sort<S: Sort>(&self) -> Arc<S> {
694        self.get_sort_by(|_| true)
695    }
696
697    /// Returns all sorts that satisfy the predicate.
698    pub fn get_arcsorts_by(&self, f: impl Fn(&ArcSort) -> bool) -> Vec<ArcSort> {
699        self.sorts.values().filter(|&x| f(x)).cloned().collect()
700    }
701
702    /// Returns a sort based on the predicate.
703    pub fn get_arcsort_by(&self, f: impl Fn(&ArcSort) -> bool) -> ArcSort {
704        let results = self.get_arcsorts_by(f);
705        assert_eq!(
706            results.len(),
707            1,
708            "Expected exactly one sort matching the given predicate"
709        );
710        results.into_iter().next().unwrap()
711    }
712
713    /// Returns the unique sort whose runtime values have Rust type `T`.
714    pub fn get_arcsort_for_value_type<T: 'static>(&self) -> ArcSort {
715        let results = self.get_arcsorts_by(|s| s.value_type() == Some(std::any::TypeId::of::<T>()));
716        assert_eq!(
717            results.len(),
718            1,
719            "Expected exactly one sort for type `{}`",
720            std::any::type_name::<T>()
721        );
722        results.into_iter().next().unwrap()
723    }
724
725    /// Check if a sort allows union operations.
726    /// A sort is unionable if it's an eq_sort and not marked as non-unionable
727    /// (e.g., from `(sort Foo :no-union)` or relation desugaring).
728    pub fn is_sort_unionable(&self, sort: &ArcSort) -> bool {
729        sort.is_eq_sort() && !self.non_unionable_sorts.contains(sort.name())
730    }
731
732    fn function_to_functype(&self, func: &FunctionDecl) -> Result<FuncType, TypeError> {
733        let input = func
734            .schema
735            .input
736            .iter()
737            .map(|name| {
738                if let Some(sort) = self.sorts.get(name) {
739                    Ok(sort.clone())
740                } else {
741                    Err(TypeError::UndefinedSort(name.clone(), func.span.clone()))
742                }
743            })
744            .collect::<Result<Vec<_>, _>>()?;
745        let output = if let Some(sort) = self.sorts.get(&func.schema.output) {
746            Ok(sort.clone())
747        } else {
748            Err(TypeError::UndefinedSort(
749                func.schema.output.clone(),
750                func.span.clone(),
751            ))
752        }?;
753
754        Ok(FuncType {
755            name: func.name.clone(),
756            subtype: func.subtype,
757            input,
758            output: output.clone(),
759        })
760    }
761
762    fn typecheck_function(
763        &mut self,
764        symbol_gen: &mut SymbolGen,
765        fdecl: &FunctionDecl,
766    ) -> Result<ResolvedFunctionDecl, TypeError> {
767        if self.sorts.contains_key(&fdecl.name) {
768            return Err(TypeError::SortAlreadyBound(
769                fdecl.name.clone(),
770                fdecl.span.clone(),
771            ));
772        }
773        if self.is_primitive(&fdecl.name) {
774            return Err(TypeError::PrimitiveAlreadyBound(
775                fdecl.name.clone(),
776                fdecl.span.clone(),
777            ));
778        }
779        // View tables (with term_constructor) must have at least one input (the e-class)
780        if fdecl.term_constructor.is_some() && fdecl.schema.input.is_empty() {
781            return Err(TypeError::TermConstructorNoInputs(
782                fdecl.name.clone(),
783                fdecl.span.clone(),
784            ));
785        }
786        let ftype = self.function_to_functype(fdecl)?;
787        if self.func_types.insert(fdecl.name.clone(), ftype).is_some() {
788            return Err(TypeError::FunctionAlreadyBound(
789                fdecl.name.clone(),
790                fdecl.span.clone(),
791            ));
792        }
793        let mut bound_vars = IndexMap::default();
794        let output_type = self.sorts.get(&fdecl.schema.output).unwrap();
795        if fdecl.subtype == FunctionSubtype::Constructor && !output_type.is_eq_sort() {
796            return Err(TypeError::ConstructorOutputNotSort(
797                fdecl.name.clone(),
798                fdecl.span.clone(),
799            ));
800        }
801        bound_vars.insert("old", (fdecl.span.clone(), output_type.clone()));
802        bound_vars.insert("new", (fdecl.span.clone(), output_type.clone()));
803
804        Ok(ResolvedFunctionDecl {
805            name: fdecl.name.clone(),
806            subtype: fdecl.subtype,
807            schema: fdecl.schema.clone(),
808            resolved_schema: ResolvedCall::Func(self.func_types.get(&fdecl.name).unwrap().clone()),
809            merge: match &fdecl.merge {
810                // Merge expressions run as part of action-side table updates:
811                // writes are allowed, but live DB reads would be untracked by
812                // seminaive rule execution.
813                Some(merge) => Some(self.typecheck_standalone_expr(
814                    symbol_gen,
815                    merge,
816                    &bound_vars,
817                    Context::Write,
818                )?),
819                None => None,
820            },
821            cost: fdecl.cost,
822            unextractable: fdecl.unextractable,
823            internal_hidden: fdecl.internal_hidden,
824            internal_let: fdecl.internal_let,
825            span: fdecl.span.clone(),
826            term_constructor: fdecl.term_constructor.clone(),
827        })
828    }
829
830    fn typecheck_schedule(
831        &self,
832        symbol_gen: &mut SymbolGen,
833        schedule: &Schedule,
834    ) -> Result<ResolvedSchedule, TypeError> {
835        let schedule = match schedule {
836            Schedule::Repeat(span, times, schedule) => ResolvedSchedule::Repeat(
837                span.clone(),
838                *times,
839                Box::new(self.typecheck_schedule(symbol_gen, schedule)?),
840            ),
841            Schedule::Sequence(span, schedules) => {
842                let schedules = schedules
843                    .iter()
844                    .map(|schedule| self.typecheck_schedule(symbol_gen, schedule))
845                    .collect::<Result<Vec<_>, _>>()?;
846                ResolvedSchedule::Sequence(span.clone(), schedules)
847            }
848            Schedule::Saturate(span, schedule) => ResolvedSchedule::Saturate(
849                span.clone(),
850                Box::new(self.typecheck_schedule(symbol_gen, schedule)?),
851            ),
852            Schedule::Run(span, RunConfig { ruleset, until }) => {
853                let until = until
854                    .as_ref()
855                    .map(|facts| self.typecheck_facts(symbol_gen, facts))
856                    .transpose()?;
857                ResolvedSchedule::Run(
858                    span.clone(),
859                    ResolvedRunConfig {
860                        ruleset: ruleset.clone(),
861                        until,
862                    },
863                )
864            }
865        };
866
867        Result::Ok(schedule)
868    }
869
870    fn typecheck_rule(
871        &self,
872        symbol_gen: &mut SymbolGen,
873        rule: &Rule,
874        global_seminaive: bool,
875    ) -> Result<ResolvedRule, TypeError> {
876        let Rule {
877            span,
878            head,
879            body,
880            name,
881            ruleset,
882            eval_mode,
883            no_decomp,
884            include_subsumed,
885        } = rule;
886        let mut constraints = vec![];
887
888        // Compile with the permissive Read/Full primitive contexts (so the RHS
889        // can read the database) when the whole EGraph is non-seminaive, or the
890        // rule's own mode requires it (`:naive` / `:unsafe-seminaive`).
891        let read_contexts = !global_seminaive
892            || matches!(
893                eval_mode,
894                RuleEvalMode::Naive | RuleEvalMode::UnsafeSeminaive
895            );
896        let (query_ctx, action_ctx) = if read_contexts {
897            (Context::Read, Context::Full)
898        } else {
899            (Context::Pure, Context::Write)
900        };
901
902        let (query, mapped_query) = Facts(body.clone()).to_query(self, symbol_gen);
903        constraints.extend(query.get_constraints(self, query_ctx)?);
904
905        let mut binding = query.get_vars();
906        // We lower to core actions with `union_to_set_optimization`
907        // later in the pipeline. For typechecking we do not need it.
908        let mut ctx = CoreActionContext::new(self, &mut binding, symbol_gen, false);
909        let (actions, mapped_action) = head.to_core_actions(&mut ctx)?;
910
911        let mut problem = Problem::default();
912        problem.add_rule(
913            &CoreRule {
914                span: span.clone(),
915                body: query,
916                head: actions,
917            },
918            self,
919            symbol_gen,
920            query_ctx,
921            action_ctx,
922        )?;
923
924        let assignment = problem
925            .solve(|sort: &ArcSort| sort.name())
926            .map_err(|e| e.to_type_error())?;
927
928        let body: Vec<ResolvedFact> = assignment.annotate_facts(&mapped_query, self, query_ctx);
929        let actions: ResolvedActions =
930            assignment.annotate_actions(&mapped_action, self, action_ctx)?;
931
932        // Function lookups in actions need the `Full` action context; the
933        // `Write` context (`!read_contexts`) can't express them.
934        if !read_contexts {
935            self.check_no_function_lookups_in_actions(&actions)?;
936        }
937
938        Ok(ResolvedRule {
939            span: span.clone(),
940            body,
941            head: actions,
942            name: name.clone(),
943            ruleset: ruleset.clone(),
944            eval_mode: *eval_mode,
945            no_decomp: *no_decomp,
946            include_subsumed: *include_subsumed,
947        })
948    }
949
950    fn check_lookup_expr(&self, expr: &ResolvedExpr) -> Result<(), TypeError> {
951        if let Some(span) = self.expr_has_function_lookup(expr) {
952            return Err(TypeError::LookupInRuleDisallowed(
953                "function".to_string(),
954                span,
955            ));
956        }
957        Ok(())
958    }
959
960    fn check_no_function_lookups_in_actions(
961        &self,
962        actions: &ResolvedActions,
963    ) -> Result<(), TypeError> {
964        for action in actions.iter() {
965            match action {
966                GenericAction::Let(_, _, rhs) => self.check_lookup_expr(rhs)?,
967                GenericAction::Set(_, _, args, rhs) => {
968                    for arg in args.iter() {
969                        self.check_lookup_expr(arg)?;
970                    }
971                    self.check_lookup_expr(rhs)?;
972                }
973                GenericAction::Union(_, lhs, rhs) => {
974                    self.check_lookup_expr(lhs)?;
975                    self.check_lookup_expr(rhs)?;
976                }
977                GenericAction::Change(_, _, _, args) => {
978                    for arg in args.iter() {
979                        self.check_lookup_expr(arg)?;
980                    }
981                }
982                GenericAction::Panic(..) => {}
983                GenericAction::Expr(_, expr) => self.check_lookup_expr(expr)?,
984            }
985        }
986        Ok(())
987    }
988
989    pub fn typecheck_facts(
990        &self,
991        symbol_gen: &mut SymbolGen,
992        facts: &[Fact],
993    ) -> Result<Vec<ResolvedFact>, TypeError> {
994        let (query, mapped_facts) = Facts(facts.to_vec()).to_query(self, symbol_gen);
995        let mut problem = Problem::default();
996        // Top-level query-shaped commands (e.g. `check`) are read-only:
997        // primitives may inspect the database but not write to it.
998        problem.add_query(&query, self, Context::Read)?;
999        let assignment = problem
1000            .solve(|sort: &ArcSort| sort.name())
1001            .map_err(|e| e.to_type_error())?;
1002        let annotated_facts = assignment.annotate_facts(&mapped_facts, self, Context::Read);
1003        Ok(annotated_facts)
1004    }
1005
1006    // Standalone expressions/actions use action lowering. Top-level commands
1007    // pass `Full`; function `:merge` reuses this path with `Write` because
1008    // merge expressions run during table updates.
1009    fn typecheck_standalone_actions(
1010        &self,
1011        symbol_gen: &mut SymbolGen,
1012        actions: &Actions,
1013        binding: &IndexMap<&str, (Span, ArcSort)>,
1014        context: Context,
1015    ) -> Result<ResolvedActions, TypeError> {
1016        let mut binding_set: IndexSet<String> =
1017            binding.keys().copied().map(str::to_string).collect();
1018        // We lower to core actions with `union_to_set_optimization`
1019        // later in the pipeline. For typechecking we do not need it.
1020        let mut ctx = CoreActionContext::new(self, &mut binding_set, symbol_gen, false);
1021        let (actions, mapped_action) = actions.to_core_actions(&mut ctx)?;
1022        let mut problem = Problem::default();
1023
1024        problem.add_actions(&actions, self, symbol_gen, context)?;
1025
1026        // add bindings from the context
1027        for (var, (span, sort)) in binding {
1028            problem.assign_local_var_type(var, span.clone(), sort.clone())?;
1029        }
1030
1031        let assignment = problem
1032            .solve(|sort: &ArcSort| sort.name())
1033            .map_err(|e| e.to_type_error())?;
1034
1035        let annotated_actions = assignment.annotate_actions(&mapped_action, self, context)?;
1036        Ok(annotated_actions)
1037    }
1038
1039    fn typecheck_standalone_expr(
1040        &self,
1041        symbol_gen: &mut SymbolGen,
1042        expr: &Expr,
1043        binding: &IndexMap<&str, (Span, ArcSort)>,
1044        context: Context,
1045    ) -> Result<ResolvedExpr, TypeError> {
1046        let action = Action::Expr(expr.span(), expr.clone());
1047        let typechecked_action =
1048            self.typecheck_standalone_action(symbol_gen, &action, binding, context)?;
1049        match typechecked_action {
1050            ResolvedAction::Expr(_, expr) => Ok(expr),
1051            _ => unreachable!(),
1052        }
1053    }
1054
1055    pub(crate) fn typecheck_expr_with_output(
1056        &self,
1057        symbol_gen: &mut SymbolGen,
1058        expr: &Expr,
1059        binding: &IndexMap<&str, (Span, ArcSort)>,
1060        output_sort: ArcSort,
1061        context: Context,
1062    ) -> Result<ResolvedExpr, TypeError> {
1063        let action = Action::Expr(expr.span(), expr.clone());
1064        let mut binding_set: IndexSet<String> =
1065            binding.keys().copied().map(str::to_string).collect();
1066        let mut ctx = CoreActionContext::new(self, &mut binding_set, symbol_gen, false);
1067        let (actions, mapped_action) = Actions::singleton(action).to_core_actions(&mut ctx)?;
1068        let mut problem = Problem::default();
1069
1070        problem.add_actions(&actions, self, symbol_gen, context)?;
1071
1072        for (var, (span, sort)) in binding {
1073            problem.assign_local_var_type(var, span.clone(), sort.clone())?;
1074        }
1075
1076        let [GenericAction::Expr(_, mapped_expr)] = mapped_action.0.as_slice() else {
1077            unreachable!("typechecking an expression should produce one expression action")
1078        };
1079        let output_atom = mapped_expr.get_corresponding_var_or_lit(self);
1080        problem.add_binding(output_atom, output_sort.clone());
1081
1082        let assignment = problem
1083            .solve(|sort: &ArcSort| sort.name())
1084            .map_err(|e| e.to_type_error())?;
1085
1086        let annotated_actions = assignment.annotate_actions(&mapped_action, self, context)?;
1087        match annotated_actions.0.into_iter().next().unwrap() {
1088            ResolvedAction::Expr(_, resolved_expr) => {
1089                let actual = resolved_expr.output_type();
1090                if actual.name() != output_sort.name() {
1091                    return Err(TypeError::Mismatch {
1092                        expr: expr.clone(),
1093                        expected: output_sort,
1094                        actual,
1095                    });
1096                }
1097                Ok(resolved_expr)
1098            }
1099            _ => unreachable!(),
1100        }
1101    }
1102
1103    fn typecheck_standalone_action(
1104        &self,
1105        symbol_gen: &mut SymbolGen,
1106        action: &Action,
1107        binding: &IndexMap<&str, (Span, ArcSort)>,
1108        context: Context,
1109    ) -> Result<ResolvedAction, TypeError> {
1110        self.typecheck_standalone_actions(
1111            symbol_gen,
1112            &Actions::singleton(action.clone()),
1113            binding,
1114            context,
1115        )
1116        .map(|v| {
1117            assert_eq!(v.len(), 1);
1118            v.0.into_iter().next().unwrap()
1119        })
1120    }
1121
1122    pub fn get_sort_by_name(&self, sym: &str) -> Option<&ArcSort> {
1123        self.sorts.get(sym)
1124    }
1125
1126    pub fn get_prims(&self, sym: &str) -> Option<&[PrimitiveWithId]> {
1127        self.primitives.get(sym).map(Vec::as_slice)
1128    }
1129
1130    pub fn is_primitive(&self, sym: &str) -> bool {
1131        self.primitives.contains_key(sym) || self.reserved_primitives.contains(sym)
1132    }
1133
1134    pub fn primitive_has_validator(&self, id: ExternalFunctionId) -> bool {
1135        self.primitives
1136            .values()
1137            .flat_map(|v| v.iter())
1138            .any(|p| p.context_ids.iter().any(|(_, pid)| *pid == Some(id)) && p.validator.is_some())
1139    }
1140
1141    pub fn get_func_type(&self, sym: &str) -> Option<&FuncType> {
1142        self.func_types.get(sym)
1143    }
1144
1145    pub fn is_constructor(&self, sym: &str) -> bool {
1146        self.func_types
1147            .get(sym)
1148            .is_some_and(|f| f.subtype == FunctionSubtype::Constructor)
1149    }
1150
1151    pub fn get_global_sort(&self, sym: &str) -> Option<&ArcSort> {
1152        self.global_sorts.get(sym)
1153    }
1154
1155    pub fn is_global(&self, sym: &str) -> bool {
1156        self.global_sorts.contains_key(sym)
1157    }
1158
1159    /// Check if an expression contains non-global function lookups (FunctionSubtype::Custom calls).
1160    /// Global function calls are allowed since they get desugared to constructors.
1161    /// Returns Some(span) if a lookup is found, None otherwise.
1162    pub fn expr_has_function_lookup(&self, expr: &ResolvedExpr) -> Option<Span> {
1163        use ast::GenericExpr;
1164
1165        expr.find(&mut |e| {
1166            if let GenericExpr::Call(span, ResolvedCall::Func(func_type), _) = e
1167                && func_type.subtype == FunctionSubtype::Custom
1168                && !self.is_global(&func_type.name)
1169            {
1170                return Some(span.clone());
1171            }
1172            None
1173        })
1174    }
1175}
1176
1177#[derive(Debug, Clone, Error)]
1178pub enum TypeError {
1179    #[error("{}\nArity mismatch, expected {expected} args: {expr}", .expr.span())]
1180    Arity { expr: Expr, expected: usize },
1181    #[error(
1182        "{}\n Expect expression {expr} to have type {}, but get type {}",
1183        .expr.span(), .expected.name(), .actual.name(),
1184    )]
1185    Mismatch {
1186        expr: Expr,
1187        expected: ArcSort,
1188        actual: ArcSort,
1189    },
1190    #[error("{1}\nUnbound symbol {0}")]
1191    Unbound(String, Span),
1192    #[error(
1193        "{1}\nVariable {0} is ungrounded. A variable is grounded when it appears as an argument to a constructor or function in the query, not just under primitives or equalities."
1194    )]
1195    Ungrounded(String, Span),
1196    #[error("{1}\nUndefined sort {0}")]
1197    UndefinedSort(String, Span),
1198    #[error("{1}\nUnbound function {0}")]
1199    UnboundFunction(String, Span),
1200    #[error("{1}\nprove-exists requires constructor function, but {0} is not a constructor")]
1201    ProveExistsRequiresConstructor(String, Span),
1202    #[error("{1}\nFunction already bound {0}")]
1203    FunctionAlreadyBound(String, Span),
1204    #[error("{1}\nSort {0} already declared.")]
1205    SortAlreadyBound(String, Span),
1206    #[error("{1}\nPrimitive {0} already declared.")]
1207    PrimitiveAlreadyBound(String, Span),
1208    #[error("Function type mismatch: expected {} => {}, actual {} => {}", .1.iter().map(|s| s.name().to_string()).collect::<Vec<_>>().join(", "), .0.name(), .3.iter().map(|s| s.name().to_string()).collect::<Vec<_>>().join(", "), .2.name())]
1209    FunctionTypeMismatch(ArcSort, Vec<ArcSort>, ArcSort, Vec<ArcSort>),
1210    #[error("{1}\nPresort {0} not found.")]
1211    PresortNotFound(String, Span),
1212    #[error("{}\nFailed to infer a type for: {}", .0.span(), .0)]
1213    InferenceFailure(Expr),
1214    #[error("{1}\nVariable {0} was already defined")]
1215    AlreadyDefined(String, Span),
1216    #[error("{1}\nThe output type of constructor function {0} must be sort")]
1217    ConstructorOutputNotSort(String, Span),
1218    #[error("{1}\nValue lookup of non-constructor function {0} in rule is disallowed.")]
1219    LookupInRuleDisallowed(String, Span),
1220    #[error("{1}\nCannot set constructor {0}. Use `union` instead or declare {0} as a function.")]
1221    SetConstructorDisallowed(String, Span),
1222    #[error("All alternative definitions considered failed\n{}", .0.iter().map(|e| format!("  {e}\n")).collect::<Vec<_>>().join(""))]
1223    AllAlternativeFailed(Vec<TypeError>),
1224    #[error("{}\nCannot union values of sort {}", .1, .0.name())]
1225    NonEqsortUnion(ArcSort, Span),
1226    #[error("{}\nCannot union values of sort {} because it is marked as non-unionable (e.g. from a relation)", .1, .0.name())]
1227    NonUnionableSort(ArcSort, Span),
1228    #[error(
1229        "{1}\nView table {0} with :internal-term-constructor must have at least one input (the e-class)."
1230    )]
1231    TermConstructorNoInputs(String, Span),
1232    #[error(
1233        "{span}\nNon-global variable `{name}` must not start with `{}`.",
1234        crate::GLOBAL_NAME_PREFIX
1235    )]
1236    NonGlobalPrefixed { name: String, span: Span },
1237    #[error(
1238        "{span}\nGlobal `{name}` must start with `{}`.",
1239        crate::GLOBAL_NAME_PREFIX
1240    )]
1241    GlobalMissingPrefix { name: String, span: Span },
1242}
1243
1244#[cfg(test)]
1245mod test {
1246    use crate::{EGraph, Error, typechecking::TypeError};
1247
1248    #[test]
1249    fn test_arity_mismatch() {
1250        let mut egraph = EGraph::default();
1251
1252        let prog = "
1253            (relation f (i64 i64))
1254            (rule ((f a b c)) ())
1255       ";
1256        let res = egraph.parse_and_run_program(None, prog);
1257        match res {
1258            Err(Error::TypeError(TypeError::Arity {
1259                expected: 2,
1260                expr: e,
1261            })) => {
1262                assert_eq!(e.span().string(), "(f a b c)");
1263            }
1264            _ => panic!("Expected arity mismatch, got: {res:?}"),
1265        }
1266    }
1267}