Skip to main content

jia_parse/jia_lang/
analysis.rs

1//! Symbol table for the CP scheduling modelling language.
2//!
3//! Builds a [`SymbolTable`] from a parsed [`JiaModel`] and its token stream,
4//! mapping declared variable names to their types, declaration spans, usage
5//! spans, and domain summaries. Used by the LSP server for hover, go-to-definition,
6//! and completion.
7
8use std::collections::HashMap;
9
10use crate::error::Span;
11use crate::jia_lang::ast::{Domain, DomainStmt, Expr, JiaModel, VarType};
12use crate::jia_lang::lexer::{Token, TokenKind};
13
14/// Information about a single declared variable.
15#[derive(Debug, Clone)]
16pub struct SymbolInfo {
17    /// The variable's declared type.
18    pub var_type: VarType,
19    /// Span of the name token in the variables block (declaration site).
20    pub decl_span: Span,
21    /// All spans where this name appears as a reference (domains, constraints, exprs).
22    pub ref_spans: Vec<Span>,
23    /// Human-readable domain summary for hover display (e.g. "duration = 3, start in 0..10").
24    pub domain_summary: Option<String>,
25}
26
27/// Maps variable names to their declaration and usage information.
28#[derive(Debug, Clone)]
29pub struct SymbolTable {
30    /// Variable name → symbol info.
31    pub symbols: HashMap<String, SymbolInfo>,
32}
33
34/// Compute the text length (in bytes) of a token, for span-range calculations.
35pub fn token_text_len(token: &Token) -> usize {
36    match &token.kind {
37        TokenKind::Ident(s) => s.len(),
38        TokenKind::Number(n) => {
39            if *n == 0 {
40                1
41            } else {
42                let abs = n.unsigned_abs();
43                let digits = (abs as f64).log10().floor() as usize + 1;
44                if *n < 0 {
45                    digits + 1
46                } else {
47                    digits
48                }
49            }
50        }
51        TokenKind::LParen
52        | TokenKind::RParen
53        | TokenKind::LBrace
54        | TokenKind::RBrace
55        | TokenKind::LBracket
56        | TokenKind::RBracket
57        | TokenKind::Comma
58        | TokenKind::Colon
59        | TokenKind::Plus
60        | TokenKind::Minus
61        | TokenKind::Star
62        | TokenKind::Lt
63        | TokenKind::Gt
64        | TokenKind::Eq
65        | TokenKind::Slash
66        | TokenKind::At => 1,
67        TokenKind::DotDot | TokenKind::Le | TokenKind::Ge | TokenKind::EqEq | TokenKind::Ne => 2,
68        TokenKind::Float(f) => format!("{f}").len(),
69    }
70}
71
72/// Build a [`SymbolTable`] from a successfully parsed model and its token stream.
73///
74/// This performs a post-parse scan of the tokens to find declaration and reference
75/// spans, then extracts domain summaries from the AST. No changes to the parser
76/// or AST types are required.
77pub fn build_symbol_table(model: &JiaModel, tokens: &[Token]) -> SymbolTable {
78    // Step 1: Collect declared variable names and types from the AST.
79    let mut declared: HashMap<String, VarType> = HashMap::new();
80    for decl in &model.variables {
81        for name in &decl.names {
82            declared.insert(name.clone(), decl.var_type.clone());
83        }
84    }
85
86    // Step 2: Scan the token stream to find declaration vs. reference spans.
87    // Declaration spans are ident tokens inside the variables block;
88    // reference spans are ident tokens matching a declared name everywhere else.
89    let mut symbols: HashMap<String, SymbolInfo> = HashMap::new();
90    let mut in_variables_block = false;
91    let mut brace_depth: usize = 0;
92    let mut variables_brace_depth: Option<usize> = None;
93
94    for token in tokens {
95        match &token.kind {
96            TokenKind::Ident(name) if name == "variables" => {
97                in_variables_block = true;
98            }
99            TokenKind::LBrace => {
100                brace_depth += 1;
101                if in_variables_block && variables_brace_depth.is_none() {
102                    variables_brace_depth = Some(brace_depth);
103                }
104            }
105            TokenKind::RBrace => {
106                if let Some(vbd) = variables_brace_depth {
107                    if brace_depth == vbd {
108                        in_variables_block = false;
109                        variables_brace_depth = None;
110                    }
111                }
112                brace_depth = brace_depth.saturating_sub(1);
113            }
114            TokenKind::Ident(name) if declared.contains_key(name) => {
115                let entry = symbols.entry(name.clone()).or_insert_with(|| SymbolInfo {
116                    var_type: declared[name].clone(),
117                    decl_span: token.span,
118                    ref_spans: Vec::new(),
119                    domain_summary: None,
120                });
121                if in_variables_block {
122                    // First occurrence in variables block is the declaration.
123                    entry.decl_span = token.span;
124                } else {
125                    entry.ref_spans.push(token.span);
126                }
127            }
128            _ => {}
129        }
130    }
131
132    // Step 3: Build domain summaries from the AST.
133    for stmt in &model.domains {
134        match stmt {
135            DomainStmt::IntervalDuration { intervals, domain } => {
136                let desc = format!("duration {}", format_domain(domain));
137                for name in intervals {
138                    append_domain_summary(&mut symbols, name, &desc);
139                }
140            }
141            DomainStmt::IntervalStart { intervals, domain } => {
142                let desc = format!("start {}", format_domain(domain));
143                for name in intervals {
144                    append_domain_summary(&mut symbols, name, &desc);
145                }
146            }
147            DomainStmt::IntervalEnd { intervals, domain } => {
148                let desc = format!("end {}", format_domain(domain));
149                for name in intervals {
150                    append_domain_summary(&mut symbols, name, &desc);
151                }
152            }
153            DomainStmt::IntervalOptional { intervals } => {
154                for name in intervals {
155                    append_domain_summary(&mut symbols, name, "optional");
156                }
157            }
158            DomainStmt::IntegerDomain { name, domain } => {
159                let desc = format_domain(domain);
160                append_domain_summary(&mut symbols, name, &desc);
161            }
162            DomainStmt::SetDomain { name, members } => {
163                let desc = format!("members: {{{}}}", members.join(", "));
164                append_domain_summary(&mut symbols, name, &desc);
165            }
166            DomainStmt::Demand {
167                interval,
168                set,
169                value,
170            } => {
171                let desc = format!("demand({set}) = {value}");
172                append_domain_summary(&mut symbols, interval, &desc);
173            }
174            DomainStmt::RealDomain { name, domain } => {
175                let desc = format!("real {}", format_domain(domain));
176                append_domain_summary(&mut symbols, name, &desc);
177            }
178        }
179    }
180
181    SymbolTable { symbols }
182}
183
184/// Format a [`Domain`] as a human-readable string.
185fn format_domain(domain: &Domain) -> String {
186    match domain {
187        Domain::Fixed(v) => format!("= {v}"),
188        Domain::Range { min, max } => format!("in {min}..{max}"),
189        Domain::Enumerated(vals) => {
190            let vals_str: Vec<String> = vals.iter().map(|v| v.to_string()).collect();
191            format!("in {{{}}}", vals_str.join(", "))
192        }
193        Domain::RealFixed(v) => format!("= {v}"),
194        Domain::RealRange { min, max } => format!("in {min}..{max}"),
195    }
196}
197
198/// Append a domain description line to a symbol's domain summary.
199fn append_domain_summary(symbols: &mut HashMap<String, SymbolInfo>, name: &str, desc: &str) {
200    if let Some(info) = symbols.get_mut(name) {
201        match &mut info.domain_summary {
202            Some(existing) => {
203                existing.push_str(", ");
204                existing.push_str(desc);
205            }
206            None => {
207                info.domain_summary = Some(desc.to_string());
208            }
209        }
210    }
211}
212
213/// Find the token at a given 0-based line and character position.
214///
215/// Returns the token whose span covers the given position, or `None` if no
216/// token is at that position.
217pub fn token_at_position(tokens: &[Token], line: u32, character: u32) -> Option<&Token> {
218    // Convert from 0-based LSP position to 1-based Span position.
219    let target_line = line as usize + 1;
220    let target_col = character as usize + 1;
221
222    for token in tokens {
223        if token.span.line == target_line {
224            let start_col = token.span.col;
225            let end_col = start_col + token_text_len(token);
226            if target_col >= start_col && target_col < end_col {
227                return Some(token);
228            }
229        }
230    }
231    None
232}
233
234/// Collect all variable names referenced inside an expression.
235fn _collect_expr_names(expr: &Expr, names: &mut Vec<String>) {
236    match expr {
237        Expr::Var(name)
238        | Expr::StartOf(name)
239        | Expr::EndOf(name)
240        | Expr::DurationOf(name)
241        | Expr::PresentOf(name) => {
242            names.push(name.clone());
243        }
244        Expr::BinaryOp { left, right, .. } => {
245            _collect_expr_names(left, names);
246            _collect_expr_names(right, names);
247        }
248        Expr::Negate(inner) => {
249            _collect_expr_names(inner, names);
250        }
251        Expr::Number(_) | Expr::Float(_) => {}
252    }
253}
254
255impl std::fmt::Display for VarType {
256    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
257        match self {
258            VarType::Interval => write!(f, "Interval"),
259            VarType::Integer => write!(f, "Integer"),
260            VarType::SetInterval => write!(f, "Set[Interval]"),
261            VarType::SetInteger => write!(f, "Set[Integer]"),
262            VarType::Real => write!(f, "Real"),
263        }
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270    use crate::error::Span;
271    use crate::jia_lang::ast::{ArithOp, Expr, VarDecl};
272    use crate::jia_lang::lexer::tokenize;
273    use crate::jia_lang::parser::parse_model;
274
275    fn build_table(input: &str) -> (JiaModel, SymbolTable) {
276        let tokens = tokenize(input).unwrap();
277        let model = parse_model(&tokens).unwrap();
278        let table = build_symbol_table(&model, &tokens);
279        (model, table)
280    }
281
282    #[test]
283    fn test_basic_symbol_table() {
284        let input = r#"
285model test
286variables {
287  Interval: a, b
288  Integer: x
289}
290domains {
291  duration(a) = 3
292  x in 0..10
293}
294constraints {
295  end_of(a) <= start_of(b)
296  end_of(b) <= x
297}
298"#;
299        let (_model, table) = build_table(input);
300
301        // Check all variables are in the table.
302        assert!(table.symbols.contains_key("a"));
303        assert!(table.symbols.contains_key("b"));
304        assert!(table.symbols.contains_key("x"));
305
306        // Check types.
307        assert_eq!(table.symbols["a"].var_type, VarType::Interval);
308        assert_eq!(table.symbols["b"].var_type, VarType::Interval);
309        assert_eq!(table.symbols["x"].var_type, VarType::Integer);
310
311        // Check reference counts:
312        // 'a' is referenced in: duration(a), end_of(a) => 2 refs
313        assert_eq!(table.symbols["a"].ref_spans.len(), 2);
314        // 'b' is referenced in: start_of(b), end_of(b) => 2 refs
315        assert_eq!(table.symbols["b"].ref_spans.len(), 2);
316        // 'x' is referenced in: x in 0..10, end_of(b) <= x => 2 refs
317        assert_eq!(table.symbols["x"].ref_spans.len(), 2);
318
319        // Check domain summaries.
320        assert_eq!(
321            table.symbols["a"].domain_summary.as_deref(),
322            Some("duration = 3")
323        );
324        assert_eq!(
325            table.symbols["x"].domain_summary.as_deref(),
326            Some("in 0..10")
327        );
328        assert!(table.symbols["b"].domain_summary.is_none());
329    }
330
331    #[test]
332    fn test_decl_span_points_to_variables_block() {
333        let input = "model t\nvariables { Interval: task }\nconstraints { end_of(task) <= 10 }";
334        let (_model, table) = build_table(input);
335
336        let info = &table.symbols["task"];
337        // 'task' declared on line 2 (the variables line).
338        assert_eq!(info.decl_span.line, 2);
339        // Referenced once in constraints.
340        assert_eq!(info.ref_spans.len(), 1);
341        assert_eq!(info.ref_spans[0].line, 3);
342    }
343
344    #[test]
345    fn test_token_at_position() {
346        let input = "model test";
347        let tokens = tokenize(input).unwrap();
348        // 'model' is at line 0 (0-based), col 0 (0-based).
349        let tok = token_at_position(&tokens, 0, 0).unwrap();
350        assert_eq!(tok.kind, TokenKind::Ident("model".to_string()));
351
352        // 'test' is at line 0, col 6.
353        let tok = token_at_position(&tokens, 0, 6).unwrap();
354        assert_eq!(tok.kind, TokenKind::Ident("test".to_string()));
355
356        // Nothing at col 5 (space).
357        assert!(token_at_position(&tokens, 0, 5).is_none());
358        assert!(token_at_position(&tokens, 1, 0).is_none());
359    }
360
361    #[test]
362    fn test_token_text_len() {
363        let tokens = tokenize("model 42 <= ..").unwrap();
364        assert_eq!(token_text_len(&tokens[0]), 5); // "model"
365        assert_eq!(token_text_len(&tokens[1]), 2); // "42"
366        assert_eq!(token_text_len(&tokens[2]), 2); // "<="
367        assert_eq!(token_text_len(&tokens[3]), 2); // ".."
368
369        assert_eq!(
370            token_text_len(&Token {
371                kind: TokenKind::Number(0),
372                span: Span::new(0, 1, 1)
373            }),
374            1
375        );
376        assert_eq!(
377            token_text_len(&Token {
378                kind: TokenKind::Number(-123),
379                span: Span::new(0, 1, 1)
380            }),
381            4
382        );
383        assert_eq!(
384            token_text_len(&Token {
385                kind: TokenKind::Float(12.5),
386                span: Span::new(0, 1, 1)
387            }),
388            4
389        );
390        assert_eq!(
391            token_text_len(&Token {
392                kind: TokenKind::At,
393                span: Span::new(0, 1, 1)
394            }),
395            1
396        );
397    }
398
399    #[test]
400    fn test_domain_summary_multiple_entries() {
401        let input = r#"
402model test
403variables { Interval: a }
404domains {
405  duration(a) = 5
406  start(a) in 0..10
407  optional(a)
408}
409"#;
410        let (_model, table) = build_table(input);
411        let summary = table.symbols["a"].domain_summary.as_deref().unwrap();
412        assert!(summary.contains("duration = 5"));
413        assert!(summary.contains("start in 0..10"));
414        assert!(summary.contains("optional"));
415    }
416
417    #[test]
418    fn test_set_domain_summary() {
419        let input = r#"
420model test
421variables {
422  Interval: t1, t2
423  Set[Interval]: machine
424}
425domains {
426  machine = {t1, t2}
427}
428"#;
429        let (_model, table) = build_table(input);
430        let summary = table.symbols["machine"].domain_summary.as_deref().unwrap();
431        assert_eq!(summary, "members: {t1, t2}");
432    }
433
434    #[test]
435    fn test_demand_summary() {
436        let input = r#"
437model test
438variables {
439  Interval: a
440  Set[Interval]: res
441}
442domains {
443  res = {a}
444  demand(a, res) = 3
445}
446"#;
447        let (_model, table) = build_table(input);
448        let summary = table.symbols["a"].domain_summary.as_deref().unwrap();
449        assert!(summary.contains("demand(res) = 3"));
450    }
451
452    #[test]
453    fn test_real_domain_summary_and_missing_append_target() {
454        let mut symbols = HashMap::new();
455        append_domain_summary(&mut symbols, "missing", "ignored");
456
457        let input = r#"
458model test
459variables { Real: rate, slack }
460domains {
461  rate in 1.5..3.5
462  slack in -inf..inf
463}
464"#;
465        let (_model, table) = build_table(input);
466        assert_eq!(
467            table.symbols["rate"].domain_summary.as_deref(),
468            Some("in 1.5..3.5")
469        );
470        assert_eq!(
471            table.symbols["slack"].domain_summary.as_deref(),
472            Some("in -inf..inf")
473        );
474
475        let tokens = tokenize("model manual\nvariables { Real: exact }").unwrap();
476        let model = JiaModel {
477            model_type: None,
478            name: "manual".to_string(),
479            variables: vec![VarDecl {
480                names: vec!["exact".to_string()],
481                var_type: VarType::Real,
482            }],
483            domains: vec![DomainStmt::RealDomain {
484                name: "exact".to_string(),
485                domain: Domain::RealFixed(2.25),
486            }],
487            constraints: Vec::new(),
488            objective: None,
489        };
490        let table = build_symbol_table(&model, &tokens);
491        assert_eq!(
492            table.symbols["exact"].domain_summary.as_deref(),
493            Some("real = 2.25")
494        );
495    }
496
497    #[test]
498    fn test_collect_expr_names() {
499        let expr = Expr::BinaryOp {
500            op: ArithOp::Add,
501            left: Box::new(Expr::BinaryOp {
502                op: ArithOp::Sub,
503                left: Box::new(Expr::StartOf("a".to_string())),
504                right: Box::new(Expr::Negate(Box::new(Expr::EndOf("b".to_string())))),
505            }),
506            right: Box::new(Expr::BinaryOp {
507                op: ArithOp::Mul,
508                left: Box::new(Expr::DurationOf("c".to_string())),
509                right: Box::new(Expr::BinaryOp {
510                    op: ArithOp::Div,
511                    left: Box::new(Expr::PresentOf("d".to_string())),
512                    right: Box::new(Expr::Var("e".to_string())),
513                }),
514            }),
515        };
516        let mut names = Vec::new();
517        _collect_expr_names(&expr, &mut names);
518        assert_eq!(names, ["a", "b", "c", "d", "e"]);
519
520        _collect_expr_names(&Expr::Number(1), &mut names);
521        _collect_expr_names(&Expr::Float(1.5), &mut names);
522        assert_eq!(names, ["a", "b", "c", "d", "e"]);
523    }
524}