Skip to main content

logicaffeine_compile/analysis/
known_calls.rs

1//! Definition-time resolution of CALLED NAMES — a call to a name that exists
2//! NOWHERE in the program is a check error, not a native-build E0425 or a
3//! runtime surprise.
4//!
5//! This is a NAME pass, not a type pass: it never unifies, so it cannot change
6//! any inference outcome. A called name is KNOWN when it is any of:
7//!
8//!   - a `## To` function (user, prelude `.lg`, or `## To native`) anywhere in
9//!     the program — forward references and mutual recursion resolve, exactly
10//!     as [`super::check`]'s preregistration and the VM's `fn_index` do;
11//!   - a kernel builtin ([`crate::semantics::builtins::builtin_from_name`]) or
12//!     one of the VM's by-name intercepts (`show`, `args`, `loaded`);
13//!   - an enum variant constructor (the registry's callable spellings);
14//!   - a `pub fn` export of a mixed document's bundled proven module — proven
15//!     functions, `check_*` predicates, and constructor wrappers, in scope via
16//!     `use proven::*;` ([`proven_module_exports`], threaded through
17//!     [`check_unknown_calls_with_exports`] by the mixed compile seam);
18//!   - a name BOUND anywhere as a variable, parameter, pattern, or handle —
19//!     the closure-called-by-name form (`Let f be (x: Int) -> x. Show f(42).`).
20//!
21//! The binding rule is deliberately program-wide rather than scope-precise: a
22//! false accept merely defers to the runtime tiers' existing
23//! `Unknown function: <name>` error, while a false reject would refuse a valid
24//! program. Every runtime tier keeps its own call-time dispatch; this pass
25//! only closes the "name exists nowhere" hole, definition-independently (a
26//! call in a never-taken branch is still an error).
27//!
28//! CALLERS: the post-prelude compile seams (`compile_program_full_with_proven`,
29//! `compile_to_rust_with_registry_full` — i.e. `largo check`/`largo build` and
30//! every string-compile entry). The LSP's raw-source diagnostics pass must NOT
31//! run this (it cannot see the prelude, so every stdlib call would misfire);
32//! its deep flycheck covers unknown names through rustc translation.
33//!
34//! Both walkers match EXHAUSTIVELY — no wildcard arm — so a new `Stmt`/`Expr`
35//! variant fails this build until its call and binder positions are wired.
36
37use std::collections::HashSet;
38
39use crate::analysis::unify::TypeError;
40use crate::analysis::{TypeDef, TypeRegistry};
41use crate::ast::stmt::{
42    ClosureBody, Expr, MatchArm, Pattern, ReadSource, SelectBranch, Stmt, StringPart,
43};
44use crate::intern::{Interner, Symbol};
45
46/// Reject the first call (in source order) to a name that exists nowhere in
47/// the post-prelude program. See the module docs for the KNOWN-name rule.
48pub fn check_unknown_calls(
49    stmts: &[Stmt],
50    interner: &Interner,
51    registry: &TypeRegistry,
52) -> Result<(), TypeError> {
53    check_unknown_calls_with_exports(stmts, interner, registry, &HashSet::new())
54}
55
56/// [`check_unknown_calls`] for the mixed-document seam: `exports` is the
57/// callable surface of a bundled proven module (see [`proven_module_exports`]),
58/// whose names the imperative half calls by design (`use proven::*;`).
59pub fn check_unknown_calls_with_exports(
60    stmts: &[Stmt],
61    interner: &Interner,
62    registry: &TypeRegistry,
63    exports: &HashSet<String>,
64) -> Result<(), TypeError> {
65    let walk = harvest_calls(stmts);
66
67    let mut variants: HashSet<Symbol> = HashSet::new();
68    for (_, def) in registry.iter_types() {
69        if let TypeDef::Enum { variants: vs, .. } = def {
70            for v in vs {
71                variants.insert(v.name);
72            }
73        }
74    }
75
76    for site in &walk.calls {
77        let called = &site.function;
78        if walk.known.contains(called) || variants.contains(called) {
79            continue;
80        }
81        let name = interner.resolve(*called);
82        // The VM's by-name intercepts, resolved before any lookup on every tier.
83        if matches!(name, "show" | "args" | "loaded") {
84            continue;
85        }
86        if crate::semantics::builtins::builtin_from_name(name).is_some() {
87            continue;
88        }
89        if exports.contains(name) {
90            continue;
91        }
92        return Err(TypeError::UnknownFunction { name: *called });
93    }
94    Ok(())
95}
96
97/// The first call, in source order, to a builtin the language no longer has
98/// ([`crate::semantics::builtins::retired_builtin`]), refused in that builtin's
99/// own sentence. A name the program defines or binds is its own and is called.
100/// Every tier's front end runs this before the program runs, so each refuses
101/// the call in the same words, whether or not the call is ever reached.
102pub fn refuse_retired_calls(stmts: &[Stmt], interner: &Interner) -> Result<(), String> {
103    let walk = harvest_calls(stmts);
104    for site in &walk.calls {
105        if walk.known.contains(&site.function) {
106            continue;
107        }
108        if let Some(refusal) =
109            crate::semantics::builtins::retired_builtin(interner.resolve(site.function))
110        {
111            return Err(refusal.to_string());
112        }
113    }
114    Ok(())
115}
116
117/// The callable surface of a bundled proven module: every `pub fn` name. The
118/// Forge emits proven functions, `check_*` predicates, and non-nullary enum
119/// constructors all as `pub fn`s (a nullary constructor is a `pub const`,
120/// which is an identifier reference, not a call — outside this pass's domain).
121pub fn proven_module_exports(proven: &str) -> HashSet<String> {
122    proven
123        .lines()
124        .filter_map(|line| {
125            let rest = line.trim_start().strip_prefix("pub fn ")?;
126            let end = rest
127                .find(|c: char| !c.is_alphanumeric() && c != '_')
128                .unwrap_or(rest.len());
129            (end > 0).then(|| rest[..end].to_string())
130        })
131        .collect()
132}
133
134/// A by-name call site with its argument expressions, for consumers that need
135/// more than the called name (e.g. the exact-HOF call-site analysis in
136/// `codegen::bigint_promote`).
137pub(crate) struct CallSite<'a> {
138    pub(crate) function: Symbol,
139    pub(crate) args: &'a [&'a Expr<'a>],
140}
141
142/// One traversal, three harvests: every by-name call site in source order
143/// (with arguments), every name the program DEFINES or BINDS (functions,
144/// variables, params, patterns, handles, zone names, case-table columns), and
145/// every bare `Identifier` reference (a name used as a VALUE — aliasing that
146/// makes its call sites statically unknowable).
147#[derive(Default)]
148pub(crate) struct Walk<'a> {
149    pub(crate) calls: Vec<CallSite<'a>>,
150    pub(crate) known: HashSet<Symbol>,
151    pub(crate) value_refs: HashSet<Symbol>,
152}
153
154/// Run the exhaustive walker over a whole program. The walker's no-wildcard
155/// matches keep every harvest complete: a new `Stmt`/`Expr` variant fails this
156/// build until its call, binder, and value-reference positions are wired.
157pub(crate) fn harvest_calls<'a>(stmts: &'a [Stmt<'a>]) -> Walk<'a> {
158    let mut walk = Walk::default();
159    walk.walk_stmts(stmts);
160    walk
161}
162
163impl<'a> Walk<'a> {
164    fn walk_stmts(&mut self, stmts: &'a [Stmt<'a>]) {
165        for stmt in stmts {
166            self.walk_stmt(stmt);
167        }
168    }
169
170    fn bind_pattern(&mut self, pattern: &Pattern) {
171        match pattern {
172            Pattern::Identifier(sym) => {
173                self.known.insert(*sym);
174            }
175            Pattern::Tuple(syms) => {
176                for sym in syms {
177                    self.known.insert(*sym);
178                }
179            }
180        }
181    }
182
183    fn walk_stmt(&mut self, stmt: &'a Stmt<'a>) {
184        match stmt {
185            // A component's actions are callable by name from its view and handlers.
186            Stmt::ComponentDef(def) => {
187                for a in &def.actions {
188                    self.known.insert(a.name);
189                }
190                for f in &def.state {
191                    self.walk_expr(f.init);
192                }
193                for ev in &def.events {
194                    self.walk_stmts(ev.body);
195                }
196                for a in &def.actions {
197                    self.walk_stmts(a.body);
198                }
199                if let Some(view) = &def.view {
200                    let (view_exprs, view_blocks) = view.exprs_and_blocks();
201                    for e in view_exprs {
202                        self.walk_expr(e);
203                    }
204                    for b in view_blocks {
205                        self.walk_stmts(b);
206                    }
207                }
208            }
209            Stmt::DirectiveDef {
210                name,
211                body,
212                removed,
213                ..
214            } => {
215                self.known.insert(*name);
216                self.walk_stmts(body);
217                if let Some(r) = removed {
218                    self.walk_stmts(r);
219                }
220            }
221            Stmt::RouteDef { body, .. } => match &**body {
222                logicaffeine_language::ast::component::RouteBody::Statements(b) => {
223                    self.walk_stmts(b)
224                }
225                logicaffeine_language::ast::component::RouteBody::View(view) => {
226                    let (view_exprs, view_blocks) = view.exprs_and_blocks();
227                    for e in view_exprs {
228                        self.walk_expr(e);
229                    }
230                    for b in view_blocks {
231                        self.walk_stmts(b);
232                    }
233                }
234            },
235            Stmt::MiddlewareDef { body, .. } => self.walk_stmts(body),
236            Stmt::RedirectDef { .. } => {}
237            // The completion action a fetch names must exist, like any other call.
238            Stmt::ServeFetches => {}
239            Stmt::Replay { actions } => {
240                for a in actions {
241                    self.known.insert(*a);
242                }
243            }
244            Stmt::GivenFetch {
245                url, status, body, ..
246            } => {
247                self.walk_expr(url);
248                self.walk_expr(status);
249                self.walk_expr(body);
250            }
251            Stmt::Fetch {
252                url, body, then, ..
253            } => {
254                self.known.insert(*then);
255                self.walk_expr(url);
256                if let Some(b) = body {
257                    self.walk_expr(b);
258                }
259            }
260            Stmt::Ui(u) => {
261                for x in u.exprs() {
262                    self.walk_expr(x);
263                }
264            }
265            Stmt::Let { var, value, .. } => {
266                self.known.insert(*var);
267                self.walk_expr(value);
268            }
269            Stmt::Set { target, value } => {
270                self.known.insert(*target);
271                self.walk_expr(value);
272            }
273            Stmt::Swap { a, b } => {
274                self.known.insert(*a);
275                self.known.insert(*b);
276            }
277            Stmt::Call { function, args } => {
278                self.calls.push(CallSite {
279                    function: *function,
280                    args,
281                });
282                for arg in args {
283                    self.walk_expr(arg);
284                }
285            }
286            Stmt::If {
287                cond,
288                then_block,
289                else_block,
290            } => {
291                self.walk_expr(cond);
292                self.walk_stmts(then_block);
293                if let Some(else_b) = else_block {
294                    self.walk_stmts(else_b);
295                }
296            }
297            Stmt::While {
298                cond,
299                body,
300                decreasing,
301            } => {
302                self.walk_expr(cond);
303                self.walk_stmts(body);
304                if let Some(d) = decreasing {
305                    self.walk_expr(d);
306                }
307            }
308            Stmt::Repeat {
309                pattern,
310                iterable,
311                body,
312            } => {
313                self.bind_pattern(pattern);
314                self.walk_expr(iterable);
315                self.walk_stmts(body);
316            }
317            Stmt::Return { value } => {
318                if let Some(v) = value {
319                    self.walk_expr(v);
320                }
321            }
322            Stmt::Break => {}
323            // Logic-kernel bridges carry no imperative call sites.
324            Stmt::Assert { .. } | Stmt::Trust { .. } => {}
325            Stmt::RuntimeAssert {
326                condition, reason, ..
327            } => {
328                self.walk_expr(condition);
329                if let Some(r) = reason {
330                    self.walk_expr(r);
331                }
332            }
333            Stmt::Fail { reason } => self.walk_expr(reason),
334            Stmt::Give { object, recipient } | Stmt::Show { object, recipient } => {
335                self.walk_expr(object);
336                self.walk_expr(recipient);
337            }
338            Stmt::SetField { object, value, .. } => {
339                self.walk_expr(object);
340                self.walk_expr(value);
341            }
342            Stmt::StructDef { .. } => {}
343            Stmt::FunctionDef {
344                name, params, body, ..
345            } => {
346                self.known.insert(*name);
347                for (param, _) in params {
348                    self.known.insert(*param);
349                }
350                self.walk_stmts(body);
351            }
352            Stmt::TestDef { cases, body, .. } => {
353                if let Some(table) = cases {
354                    for column in &table.columns {
355                        self.known.insert(*column);
356                    }
357                    for row in &table.rows {
358                        for cell in row {
359                            self.walk_expr(cell);
360                        }
361                    }
362                }
363                self.walk_stmts(body);
364            }
365            Stmt::Expect { condition } => self.walk_expr(condition),
366            Stmt::ExpectFail { body, .. } => self.walk_stmts(body),
367            Stmt::ExpectOutput { .. } => {}
368            Stmt::AdvanceTime { millis } => self.walk_expr(millis),
369            Stmt::GivenFile { .. } => {}
370            Stmt::Observe { payloads, .. } => {
371                for (_, value) in payloads {
372                    self.walk_expr(value);
373                }
374            }
375            Stmt::During { body, .. } => self.walk_stmts(body),
376            Stmt::ForAll {
377                vars,
378                constraint,
379                body,
380                ..
381            } => {
382                for var in vars {
383                    self.known.insert(*var);
384                }
385                if let Some(c) = constraint {
386                    self.walk_expr(c);
387                }
388                self.walk_stmts(body);
389            }
390            Stmt::Inspect { target, arms, .. } => {
391                self.walk_expr(target);
392                for MatchArm { bindings, body, .. } in arms {
393                    for (_, binding) in bindings {
394                        self.known.insert(*binding);
395                    }
396                    self.walk_stmts(body);
397                }
398            }
399            Stmt::Push { value, collection }
400            | Stmt::Add { value, collection }
401            | Stmt::Remove { value, collection } => {
402                self.walk_expr(value);
403                self.walk_expr(collection);
404            }
405            Stmt::Pop { collection, into } => {
406                if let Some(var) = into {
407                    self.known.insert(*var);
408                }
409                self.walk_expr(collection);
410            }
411            Stmt::SetIndex {
412                collection,
413                index,
414                value,
415            } => {
416                self.walk_expr(collection);
417                self.walk_expr(index);
418                self.walk_expr(value);
419            }
420            Stmt::Splice { body } | Stmt::Reversibly { body } => self.walk_stmts(body),
421            Stmt::Zone { name, body, .. } => {
422                self.known.insert(*name);
423                self.walk_stmts(body);
424            }
425            Stmt::Concurrent { tasks } | Stmt::Parallel { tasks } => self.walk_stmts(tasks),
426            Stmt::ReadFrom { var, source } => {
427                self.known.insert(*var);
428                if let ReadSource::File(path) = source {
429                    self.walk_expr(path);
430                }
431            }
432            Stmt::WriteFile { content, path } => {
433                self.walk_expr(content);
434                self.walk_expr(path);
435            }
436            Stmt::Spawn { .. } => {}
437            Stmt::SendMessage {
438                message,
439                destination,
440                ..
441            } => {
442                self.walk_expr(message);
443                self.walk_expr(destination);
444            }
445            Stmt::AwaitMessage { source, into, .. } => {
446                self.known.insert(*into);
447                self.walk_expr(source);
448            }
449            Stmt::StreamMessage {
450                values,
451                destination,
452            } => {
453                self.walk_expr(values);
454                self.walk_expr(destination);
455            }
456            Stmt::MergeCrdt { source, target } | Stmt::JoinCrdt { source, target } => {
457                self.walk_expr(source);
458                self.walk_expr(target);
459            }
460            Stmt::IncreaseCrdt { object, amount, .. }
461            | Stmt::DecreaseCrdt { object, amount, .. } => {
462                self.walk_expr(object);
463                self.walk_expr(amount);
464            }
465            Stmt::AppendToSequence { sequence, value } => {
466                self.walk_expr(sequence);
467                self.walk_expr(value);
468            }
469            Stmt::ResolveConflict { object, value, .. } => {
470                self.walk_expr(object);
471                self.walk_expr(value);
472            }
473            Stmt::Check { .. } => {}
474            Stmt::Listen { address, secure } | Stmt::ConnectTo { address, secure } => {
475                self.walk_expr(address);
476                if let Some(pad) = secure {
477                    self.walk_expr(pad.pad);
478                }
479            }
480            Stmt::Serve { port, handler } => {
481                self.walk_expr(port);
482                self.walk_expr(handler);
483            }
484            Stmt::LetPeerAgent { var, address } => {
485                self.known.insert(*var);
486                self.walk_expr(address);
487            }
488            Stmt::Sleep { milliseconds } => self.walk_expr(milliseconds),
489            Stmt::Sync { var, topic } => {
490                self.known.insert(*var);
491                self.walk_expr(topic);
492            }
493            Stmt::Mount { var, path } => {
494                self.known.insert(*var);
495                self.walk_expr(path);
496            }
497            // Spawn targets are a distinct dispatch (`Unknown task function`,
498            // resolved against fn_index alone) — their ARGUMENTS are calls here.
499            Stmt::LaunchTask { args, .. } => {
500                for arg in args {
501                    self.walk_expr(arg);
502                }
503            }
504            Stmt::LaunchTaskWithHandle { handle, args, .. } => {
505                self.known.insert(*handle);
506                for arg in args {
507                    self.walk_expr(arg);
508                }
509            }
510            Stmt::CreatePipe { var, .. } => {
511                self.known.insert(*var);
512            }
513            Stmt::SendPipe { value, pipe } => {
514                self.walk_expr(value);
515                self.walk_expr(pipe);
516            }
517            Stmt::ReceivePipe { var, pipe } => {
518                self.known.insert(*var);
519                self.walk_expr(pipe);
520            }
521            Stmt::TrySendPipe {
522                value,
523                pipe,
524                result,
525            } => {
526                if let Some(var) = result {
527                    self.known.insert(*var);
528                }
529                self.walk_expr(value);
530                self.walk_expr(pipe);
531            }
532            Stmt::TryReceivePipe { var, pipe } => {
533                self.known.insert(*var);
534                self.walk_expr(pipe);
535            }
536            Stmt::StopTask { handle } => self.walk_expr(handle),
537            Stmt::CloseChannel { channel } => self.walk_expr(channel),
538            Stmt::AwaitTask { handle, result } => {
539                if let Some(var) = result {
540                    self.known.insert(*var);
541                }
542                self.walk_expr(handle);
543            }
544            Stmt::Select { branches } => {
545                for branch in branches {
546                    match branch {
547                        SelectBranch::Receive { var, pipe, body } => {
548                            self.known.insert(*var);
549                            self.walk_expr(pipe);
550                            self.walk_stmts(body);
551                        }
552                        SelectBranch::Timeout { milliseconds, body } => {
553                            self.walk_expr(milliseconds);
554                            self.walk_stmts(body);
555                        }
556                    }
557                }
558            }
559            Stmt::Theorem(_) | Stmt::Definition(_) | Stmt::Axiom(_) | Stmt::Theory(_) => {}
560            Stmt::Escape { .. } => {}
561            Stmt::Require { .. } => {}
562            // An opaque failure-site marker: no name, no child block, nothing to walk.
563            Stmt::Site(_) => {}
564        }
565    }
566
567    fn walk_expr(&mut self, expr: &'a Expr<'a>) {
568        match expr {
569            Expr::Dom(q) => {
570                if let Some(sel) = q.selector() {
571                    self.walk_expr(sel);
572                }
573            }
574            Expr::View(v) => {
575                let (view_exprs, view_blocks) = v.exprs_and_blocks();
576                for e in view_exprs {
577                    self.walk_expr(e);
578                }
579                for b in view_blocks {
580                    self.walk_stmts(b);
581                }
582            }
583            Expr::Literal(_) | Expr::OptionNone | Expr::Escape { .. } => {}
584            Expr::Identifier(sym) => {
585                self.value_refs.insert(*sym);
586            }
587            Expr::BinaryOp { left, right, .. }
588            | Expr::Union { left, right }
589            | Expr::Intersection { left, right } => {
590                self.walk_expr(left);
591                self.walk_expr(right);
592            }
593            Expr::Not { operand } => self.walk_expr(operand),
594            Expr::Call { function, args } => {
595                self.calls.push(CallSite {
596                    function: *function,
597                    args,
598                });
599                for arg in args {
600                    self.walk_expr(arg);
601                }
602            }
603            Expr::Index { collection, index } => {
604                self.walk_expr(collection);
605                self.walk_expr(index);
606            }
607            Expr::Slice {
608                collection,
609                start,
610                end,
611            } => {
612                self.walk_expr(collection);
613                self.walk_expr(start);
614                self.walk_expr(end);
615            }
616            Expr::Copy { expr } => self.walk_expr(expr),
617            Expr::Give { value } => self.walk_expr(value),
618            Expr::Length { collection } => self.walk_expr(collection),
619            Expr::Contains { collection, value } => {
620                self.walk_expr(collection);
621                self.walk_expr(value);
622            }
623            Expr::ManifestOf { zone } => self.walk_expr(zone),
624            Expr::ChunkAt { index, zone } => {
625                self.walk_expr(index);
626                self.walk_expr(zone);
627            }
628            Expr::List(items) | Expr::Tuple(items) => {
629                for item in items {
630                    self.walk_expr(item);
631                }
632            }
633            Expr::Range { start, end } => {
634                self.walk_expr(start);
635                self.walk_expr(end);
636            }
637            Expr::FieldAccess { object, .. } => self.walk_expr(object),
638            Expr::New { init_fields, .. } => {
639                for (_, value) in init_fields {
640                    self.walk_expr(value);
641                }
642            }
643            Expr::NewVariant { fields, .. } => {
644                for (_, value) in fields {
645                    self.walk_expr(value);
646                }
647            }
648            Expr::OptionSome { value } => self.walk_expr(value),
649            Expr::WithCapacity { value, capacity } => {
650                self.walk_expr(value);
651                self.walk_expr(capacity);
652            }
653            Expr::Closure { params, body, .. } => {
654                for (param, _) in params {
655                    self.known.insert(*param);
656                }
657                match body {
658                    ClosureBody::Expression(e) => self.walk_expr(e),
659                    ClosureBody::Block(stmts) => self.walk_stmts(stmts),
660                }
661            }
662            Expr::CallExpr { callee, args } => {
663                self.walk_expr(callee);
664                for arg in args {
665                    self.walk_expr(arg);
666                }
667            }
668            Expr::InterpolatedString(parts) => {
669                for part in parts {
670                    if let StringPart::Expr { value, .. } = part {
671                        self.walk_expr(value);
672                    }
673                }
674            }
675        }
676    }
677}
678
679#[cfg(test)]
680#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/analysis/known_calls/tests.rs"]
681mod tests;