1use 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#[derive(Debug, Clone)]
16pub struct SymbolInfo {
17 pub var_type: VarType,
19 pub decl_span: Span,
21 pub ref_spans: Vec<Span>,
23 pub domain_summary: Option<String>,
25}
26
27#[derive(Debug, Clone)]
29pub struct SymbolTable {
30 pub symbols: HashMap<String, SymbolInfo>,
32}
33
34pub 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
72pub fn build_symbol_table(model: &JiaModel, tokens: &[Token]) -> SymbolTable {
78 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 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 entry.decl_span = token.span;
124 } else {
125 entry.ref_spans.push(token.span);
126 }
127 }
128 _ => {}
129 }
130 }
131
132 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
184fn 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
198fn 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
213pub fn token_at_position(tokens: &[Token], line: u32, character: u32) -> Option<&Token> {
218 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
234fn _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 assert!(table.symbols.contains_key("a"));
303 assert!(table.symbols.contains_key("b"));
304 assert!(table.symbols.contains_key("x"));
305
306 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 assert_eq!(table.symbols["a"].ref_spans.len(), 2);
314 assert_eq!(table.symbols["b"].ref_spans.len(), 2);
316 assert_eq!(table.symbols["x"].ref_spans.len(), 2);
318
319 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 assert_eq!(info.decl_span.line, 2);
339 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 let tok = token_at_position(&tokens, 0, 0).unwrap();
350 assert_eq!(tok.kind, TokenKind::Ident("model".to_string()));
351
352 let tok = token_at_position(&tokens, 0, 6).unwrap();
354 assert_eq!(tok.kind, TokenKind::Ident("test".to_string()));
355
356 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); assert_eq!(token_text_len(&tokens[1]), 2); assert_eq!(token_text_len(&tokens[2]), 2); assert_eq!(token_text_len(&tokens[3]), 2); 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}