Skip to main content

maya_mel/sema/
mod.rs

1#![forbid(unsafe_code)]
2//! Generic semantic analysis for MEL syntax trees.
3//!
4//! Most users should start with [`analyze`]. It resolves proc and variable
5//! usage, emits diagnostics, and optionally normalizes command-style invokes
6//! through a caller-provided [`command_schema::CommandRegistry`]. Advanced
7//! command contracts live under [`command_schema`], and normalized command
8//! shapes live under [`command_norm`].
9
10/// Advanced command normalization data structures.
11pub mod command_norm;
12/// Advanced command schema and registry contracts.
13pub mod command_schema;
14mod flow;
15mod resolve;
16pub(crate) mod scope;
17
18#[cfg(test)]
19mod tests;
20
21pub use command_norm::NormalizedCommandInvoke;
22use command_schema::{CommandRegistry, EmptyCommandRegistry};
23
24use flow::FlowLintAnalyzer;
25use resolve::Analyzer;
26use scope::ScopeCollector;
27use std::sync::Arc;
28
29use mel_ast::{SourceFile, Stmt};
30use mel_syntax::{SourceView, TextRange};
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33/// Diagnostic severity emitted by semantic analysis.
34pub enum DiagnosticSeverity {
35    Error,
36    Warning,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40/// Filter used by diagnostics-only semantic entry points.
41pub enum DiagnosticFilter {
42    All,
43    ErrorsOnly,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47/// Semantic diagnostic with primary and secondary labels.
48///
49/// These diagnostics are produced by [`analyze`] and the diagnostics-only entry
50/// points in this module.
51pub struct Diagnostic {
52    pub severity: DiagnosticSeverity,
53    pub message: Arc<str>,
54    pub range: TextRange,
55    pub labels: Vec<DiagnosticLabel>,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
59/// A labeled span attached to a [`Diagnostic`].
60pub struct DiagnosticLabel {
61    pub range: TextRange,
62    pub message: Arc<str>,
63    pub is_primary: bool,
64}
65
66impl Diagnostic {
67    fn error(message: impl Into<Arc<str>>, range: TextRange) -> Self {
68        let message = message.into();
69        Self {
70            severity: DiagnosticSeverity::Error,
71            message: message.clone(),
72            range,
73            labels: vec![DiagnosticLabel {
74                range,
75                message,
76                is_primary: true,
77            }],
78        }
79    }
80
81    fn warning(message: impl Into<Arc<str>>, range: TextRange) -> Self {
82        let message = message.into();
83        Self {
84            severity: DiagnosticSeverity::Warning,
85            message: message.clone(),
86            range,
87            labels: vec![DiagnosticLabel {
88                range,
89                message,
90                is_primary: true,
91            }],
92        }
93    }
94
95    fn with_secondary_label(mut self, message: impl Into<Arc<str>>, range: TextRange) -> Self {
96        self.labels.push(DiagnosticLabel {
97            range,
98            message: message.into(),
99            is_primary: false,
100        });
101        self
102    }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
106/// Stable identifier for a collected lexical scope.
107pub struct ScopeId(pub(crate) usize);
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
110/// Stable identifier for a collected proc symbol.
111pub struct ProcSymbolId(pub(crate) usize);
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
114/// Stable identifier for a collected variable symbol.
115pub struct VariableSymbolId(pub(crate) usize);
116
117#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct ProcSymbol {
119    pub id: ProcSymbolId,
120    pub name_range: TextRange,
121    pub is_global: bool,
122    pub return_type: Option<mel_ast::ProcReturnType>,
123    pub owner_scope: ScopeId,
124    pub decl_order: usize,
125    pub range: TextRange,
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum VariableKind {
130    Parameter,
131    Local,
132    Global,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct VariableSymbol {
137    pub id: VariableSymbolId,
138    pub name_range: TextRange,
139    pub kind: VariableKind,
140    pub ty: mel_ast::TypeName,
141    pub is_array: bool,
142    pub owner_scope: ScopeId,
143    pub decl_order: usize,
144    pub range: TextRange,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct InvokeResolution {
149    pub range: TextRange,
150    pub scope: ScopeId,
151    pub resolution: ResolvedCallee,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
155pub enum ResolvedCallee {
156    Unresolved,
157    Proc(ProcSymbolId),
158    BuiltinCommand(Arc<str>),
159    PluginCommand(Arc<str>),
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub enum IdentTarget {
164    Unresolved,
165    Variable(VariableSymbolId),
166}
167
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct IdentResolution {
170    pub range: TextRange,
171    pub scope: ScopeId,
172    pub name_range: TextRange,
173    pub resolution: IdentTarget,
174}
175
176#[derive(Debug, Clone, PartialEq, Eq, Default)]
177/// Full semantic analysis result.
178///
179/// This bundles semantic diagnostics together with proc, variable, and invoke
180/// resolution data so downstream tools can inspect more than just errors.
181pub struct Analysis {
182    pub diagnostics: Vec<Diagnostic>,
183    pub proc_symbols: Vec<ProcSymbol>,
184    pub variable_symbols: Vec<VariableSymbol>,
185    pub invoke_resolutions: Vec<InvokeResolution>,
186    pub ident_resolutions: Vec<IdentResolution>,
187    pub normalized_invokes: Vec<NormalizedCommandInvoke>,
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
191enum AnalysisMode {
192    Full,
193    DiagnosticsOnly,
194}
195
196#[must_use]
197/// Run semantic analysis without a command registry.
198///
199/// ```rust
200/// use maya_mel::{analyze, parse_source};
201///
202/// let parse = parse_source("global proc hello() {} hello();");
203/// let analysis = analyze(&parse.syntax, parse.source_view());
204///
205/// assert!(analysis.diagnostics.is_empty());
206/// assert_eq!(analysis.proc_symbols.len(), 1);
207/// ```
208pub fn analyze(syntax: &SourceFile, source: SourceView<'_>) -> Analysis {
209    analyze_with_registry(syntax, source, &EmptyCommandRegistry)
210}
211
212#[must_use]
213/// Run semantic analysis with a caller-provided command registry.
214///
215/// ```rust
216/// use maya_mel::{MayaCommandRegistry, analyze_with_registry, parse_source};
217///
218/// let parse = parse_source("createNode transform -n \"root\";");
219/// let analysis = analyze_with_registry(&parse.syntax, parse.source_view(), &MayaCommandRegistry::new());
220///
221/// assert!(analysis.diagnostics.is_empty());
222/// assert!(!analysis.normalized_invokes.is_empty());
223/// ```
224pub fn analyze_with_registry<R>(
225    syntax: &SourceFile,
226    source: SourceView<'_>,
227    registry: &R,
228) -> Analysis
229where
230    R: CommandRegistry + ?Sized,
231{
232    analyze_with_registry_mode(
233        syntax,
234        source,
235        registry,
236        AnalysisMode::Full,
237        DiagnosticFilter::All,
238    )
239}
240
241#[must_use]
242/// Collect only diagnostics while still using a command registry.
243pub fn analyze_diagnostics_with_registry<R>(
244    syntax: &SourceFile,
245    source: SourceView<'_>,
246    registry: &R,
247) -> Vec<Diagnostic>
248where
249    R: CommandRegistry + ?Sized,
250{
251    analyze_diagnostics_with_registry_filtered(syntax, source, registry, DiagnosticFilter::All)
252}
253
254#[must_use]
255/// Collect only diagnostics with an explicit [`DiagnosticFilter`].
256pub fn analyze_diagnostics_with_registry_filtered<R>(
257    syntax: &SourceFile,
258    source: SourceView<'_>,
259    registry: &R,
260    filter: DiagnosticFilter,
261) -> Vec<Diagnostic>
262where
263    R: CommandRegistry + ?Sized,
264{
265    analyze_with_registry_mode(
266        syntax,
267        source,
268        registry,
269        AnalysisMode::DiagnosticsOnly,
270        filter,
271    )
272    .diagnostics
273}
274
275fn analyze_with_registry_mode<R>(
276    syntax: &SourceFile,
277    source: SourceView<'_>,
278    registry: &R,
279    mode: AnalysisMode,
280    filter: DiagnosticFilter,
281) -> Analysis
282where
283    R: CommandRegistry + ?Sized,
284{
285    let collected = ScopeCollector::collect(syntax);
286    let mut analyzer = Analyzer::new(
287        &collected,
288        source,
289        registry,
290        matches!(mode, AnalysisMode::Full),
291        filter,
292    );
293
294    for item in &syntax.items {
295        analyzer.walk_item(item, collected.root_scope);
296    }
297
298    let mut diagnostics = analyzer.diagnostics;
299    if matches!(filter, DiagnosticFilter::All) {
300        let mut flow_lint = FlowLintAnalyzer::new(&collected, source);
301        flow_lint.walk_source(syntax);
302        diagnostics.extend(flow_lint.diagnostics);
303    }
304
305    Analysis {
306        diagnostics,
307        proc_symbols: if matches!(mode, AnalysisMode::Full) {
308            collected.proc_symbols.clone()
309        } else {
310            Vec::new()
311        },
312        variable_symbols: if matches!(mode, AnalysisMode::Full) {
313            collected.variable_symbols.clone()
314        } else {
315            Vec::new()
316        },
317        invoke_resolutions: if matches!(mode, AnalysisMode::Full) {
318            analyzer.invoke_resolutions
319        } else {
320            Vec::new()
321        },
322        ident_resolutions: if matches!(mode, AnalysisMode::Full) {
323            analyzer.ident_resolutions
324        } else {
325            Vec::new()
326        },
327        normalized_invokes: if matches!(mode, AnalysisMode::Full) {
328            analyzer.normalized_invokes
329        } else {
330            Vec::new()
331        },
332    }
333}
334
335pub(crate) fn stmt_range(stmt: &Stmt) -> TextRange {
336    match stmt {
337        Stmt::Empty { range }
338        | Stmt::Proc { range, .. }
339        | Stmt::Block { range, .. }
340        | Stmt::Expr { range, .. }
341        | Stmt::VarDecl { range, .. }
342        | Stmt::If { range, .. }
343        | Stmt::While { range, .. }
344        | Stmt::DoWhile { range, .. }
345        | Stmt::Switch { range, .. }
346        | Stmt::For { range, .. }
347        | Stmt::ForIn { range, .. }
348        | Stmt::Return { range, .. }
349        | Stmt::Break { range }
350        | Stmt::Continue { range } => *range,
351    }
352}