Skip to main content

runmat_static_analysis/lints/
shape.rs

1use runmat_builtins::{BuiltinSemanticKind, ConcatKind, ShapeTransformKind};
2use runmat_hir::{BindingId, HirDiagnostic, HirDiagnosticSeverity, IndexKind, OperatorKind, Span};
3use runmat_mir::analysis::{AnalysisStore, MirLocalKey};
4use std::collections::HashMap;
5
6pub fn lint_shapes(result: &runmat_hir::LoweringResult) -> Vec<HirDiagnostic> {
7    let mir = match runmat_mir::lowering::lower_assembly(&result.assembly) {
8        Ok(mir) => mir,
9        Err(err) => return vec![mir_lowering_diagnostic(err)],
10    };
11    let store = runmat_mir::analysis::analyze_assembly(&mir);
12    lint_shapes_from_mir(&mir, &store)
13}
14
15pub fn lint_shapes_from_mir(
16    mir: &runmat_mir::MirAssembly,
17    store: &AnalysisStore,
18) -> Vec<HirDiagnostic> {
19    let mut ctx = ShapeLintContext::default();
20    ctx.seed_from_analysis(mir, store);
21    ctx.walk_mir_assembly(mir);
22    ctx.diagnostics
23}
24
25pub fn infer_binding_shapes(
26    result: &runmat_hir::LoweringResult,
27) -> HashMap<BindingId, Vec<Option<usize>>> {
28    let Ok(mir) = runmat_mir::lowering::lower_assembly(&result.assembly) else {
29        return HashMap::new();
30    };
31    let store = runmat_mir::analysis::analyze_assembly(&mir);
32    infer_binding_shapes_from_mir(&mir, &store)
33}
34
35pub fn infer_binding_shapes_from_mir(
36    mir: &runmat_mir::MirAssembly,
37    store: &AnalysisStore,
38) -> HashMap<BindingId, Vec<Option<usize>>> {
39    let mut ctx = ShapeLintContext::default();
40    ctx.seed_from_analysis(mir, store);
41    ctx.walk_mir_assembly(mir);
42    ctx.env
43        .into_iter()
44        .map(|(binding, shape)| (binding, shape.0))
45        .collect()
46}
47
48fn mir_lowering_diagnostic(err: runmat_hir::HirError) -> HirDiagnostic {
49    HirDiagnostic::new(
50        "lint.mir.lowering_failed",
51        HirDiagnosticSeverity::Error,
52        format!("MIR lowering failed: {}", err.message),
53        err.span.unwrap_or(runmat_hir::Span { start: 0, end: 0 }),
54    )
55    .with_category("mir-lowering")
56}
57
58#[derive(Debug, Clone, PartialEq)]
59struct Shape(Vec<Option<usize>>);
60
61#[derive(Default)]
62struct ShapeLintContext {
63    env: HashMap<BindingId, Shape>,
64    local_env: HashMap<MirLocalKey, Shape>,
65    number_env: HashMap<MirLocalKey, f64>,
66    int_vector_env: HashMap<MirLocalKey, Vec<usize>>,
67    logical_env: HashMap<MirLocalKey, bool>,
68    diagnostics: Vec<HirDiagnostic>,
69}
70
71#[derive(Default)]
72struct MirShapeValue {
73    shape: Option<Shape>,
74    number: Option<f64>,
75    int_vector: Option<Vec<usize>>,
76    logical: bool,
77}
78
79impl ShapeLintContext {
80    fn seed_from_analysis(&mut self, mir: &runmat_mir::MirAssembly, store: &AnalysisStore) {
81        for body in mir.bodies.values() {
82            for local in &body.locals {
83                let Some(binding) = local.binding else {
84                    continue;
85                };
86                let Some(fact) = store.mir_locals.get(&MirLocalKey {
87                    function: body.function,
88                    local: local.id,
89                }) else {
90                    continue;
91                };
92                if let Some(shape) = shape_from_fact(&fact.shape) {
93                    self.env.insert(binding, shape);
94                }
95            }
96        }
97    }
98
99    fn walk_mir_assembly(&mut self, mir: &runmat_mir::MirAssembly) {
100        for body in mir.bodies.values() {
101            for block in &body.blocks {
102                for stmt in &block.statements {
103                    match &stmt.kind {
104                        runmat_mir::MirStmtKind::Assign { place, value } => {
105                            let value = self.infer_mir_rvalue(body, value, stmt.span);
106                            if let runmat_mir::MirPlace::Local(local) = place {
107                                self.record_mir_value(body, *local, value);
108                            }
109                        }
110                        runmat_mir::MirStmtKind::MultiAssign { value, .. }
111                        | runmat_mir::MirStmtKind::Expr(value) => {
112                            self.infer_mir_rvalue(body, value, stmt.span);
113                        }
114                        runmat_mir::MirStmtKind::PlaceMutation(_)
115                        | runmat_mir::MirStmtKind::WorkspaceEffect { .. }
116                        | runmat_mir::MirStmtKind::EnvironmentEffect(_) => {}
117                    }
118                }
119            }
120        }
121    }
122
123    fn record_mir_value(
124        &mut self,
125        body: &runmat_mir::MirBody,
126        local: runmat_mir::MirLocalId,
127        value: MirShapeValue,
128    ) {
129        let key = MirLocalKey {
130            function: body.function,
131            local,
132        };
133        if let Some(shape) = value.shape {
134            if let Some(binding) = body.locals.get(local.0).and_then(|local| local.binding) {
135                self.env.insert(binding, shape.clone());
136            }
137            self.local_env.insert(key, shape);
138        }
139        if let Some(number) = value.number {
140            self.number_env.insert(key, number);
141        }
142        if let Some(vector) = value.int_vector {
143            self.int_vector_env.insert(key, vector);
144        }
145        if value.logical {
146            self.logical_env.insert(key, true);
147        } else {
148            self.logical_env.remove(&key);
149        }
150    }
151
152    fn infer_mir_rvalue(
153        &mut self,
154        body: &runmat_mir::MirBody,
155        value: &runmat_mir::MirRvalue,
156        span: Span,
157    ) -> MirShapeValue {
158        match value {
159            runmat_mir::MirRvalue::Use(operand) => self.infer_mir_operand(body, operand),
160            runmat_mir::MirRvalue::Unary(op, operand) => {
161                let inner = self.infer_mir_operand(body, operand);
162                let number = match (op, inner.number) {
163                    (OperatorKind::UnaryMinus, Some(value)) => Some(-value),
164                    _ => None,
165                };
166                MirShapeValue {
167                    shape: inner.shape,
168                    number,
169                    int_vector: None,
170                    logical: matches!(op, OperatorKind::Not),
171                }
172            }
173            runmat_mir::MirRvalue::Binary(left, op, right) => {
174                let lhs = self.infer_mir_operand(body, left);
175                let rhs = self.infer_mir_operand(body, right);
176                MirShapeValue {
177                    shape: self.infer_mir_binary(span, lhs.shape.as_ref(), op, rhs.shape.as_ref()),
178                    number: None,
179                    int_vector: None,
180                    logical: is_logical_operator(op),
181                }
182            }
183            runmat_mir::MirRvalue::ShortCircuit {
184                left,
185                right_temps,
186                right,
187                ..
188            } => {
189                let left_value = self.infer_mir_operand(body, left);
190                for stmt in right_temps {
191                    match &stmt.kind {
192                        runmat_mir::MirStmtKind::Assign { place, value } => {
193                            let inferred = self.infer_mir_rvalue(body, value, stmt.span);
194                            if let runmat_mir::MirPlace::Local(local) = place {
195                                self.record_mir_value(body, *local, inferred);
196                            }
197                        }
198                        runmat_mir::MirStmtKind::MultiAssign { value, .. }
199                        | runmat_mir::MirStmtKind::Expr(value) => {
200                            self.infer_mir_rvalue(body, value, stmt.span);
201                        }
202                        runmat_mir::MirStmtKind::PlaceMutation(_)
203                        | runmat_mir::MirStmtKind::WorkspaceEffect { .. }
204                        | runmat_mir::MirStmtKind::EnvironmentEffect(_) => {}
205                    }
206                }
207                let right_value = self.infer_mir_operand(body, right);
208                MirShapeValue {
209                    shape: left_value.shape.or(right_value.shape),
210                    number: None,
211                    int_vector: None,
212                    logical: true,
213                }
214            }
215            runmat_mir::MirRvalue::Range { start, step, end } => {
216                let start = self.infer_mir_operand(body, start).number;
217                let step = step
218                    .as_ref()
219                    .and_then(|step| self.infer_mir_operand(body, step).number)
220                    .unwrap_or(1.0);
221                let end = self.infer_mir_operand(body, end).number;
222                let width = start
223                    .zip(end)
224                    .and_then(|(start, end)| range_width(start, step, end));
225                MirShapeValue {
226                    shape: Some(Shape(vec![Some(1), width])),
227                    number: None,
228                    int_vector: None,
229                    logical: false,
230                }
231            }
232            runmat_mir::MirRvalue::Call(call) => self.infer_mir_call(body, span, call),
233            runmat_mir::MirRvalue::Aggregate {
234                kind,
235                rows,
236                elements,
237                ..
238            } => self.infer_mir_aggregate(body, span, kind, *rows, elements),
239            runmat_mir::MirRvalue::StructLiteral { fields } => {
240                for (_, value) in fields {
241                    self.infer_mir_operand(body, value);
242                }
243                MirShapeValue {
244                    shape: Some(Shape(vec![Some(1), Some(1)])),
245                    number: None,
246                    int_vector: None,
247                    logical: false,
248                }
249            }
250            runmat_mir::MirRvalue::ObjectLiteral { fields, .. } => {
251                for (_, value) in fields {
252                    self.infer_mir_operand(body, value);
253                }
254                MirShapeValue {
255                    shape: Some(Shape(vec![Some(1), Some(1)])),
256                    number: None,
257                    int_vector: None,
258                    logical: false,
259                }
260            }
261            runmat_mir::MirRvalue::Index { base, indexing } => {
262                let base_shape = self.infer_mir_operand(body, base).shape;
263                let mut component_values = Vec::new();
264                for component in &indexing.components {
265                    match component {
266                        runmat_mir::MirIndexComponent::Expr(operand) => {
267                            let idx_value = self.infer_mir_operand(body, operand);
268                            if indexing.components.len() == 1 && idx_value.logical {
269                                self.check_logical_index(
270                                    span,
271                                    base_shape.as_ref(),
272                                    idx_value.shape.as_ref(),
273                                );
274                            }
275                            component_values.push(Some(idx_value));
276                        }
277                        runmat_mir::MirIndexComponent::Colon
278                        | runmat_mir::MirIndexComponent::End { .. } => {
279                            component_values.push(None);
280                        }
281                    }
282                }
283                MirShapeValue {
284                    shape: infer_mir_index_shape(base_shape.as_ref(), indexing, &component_values),
285                    number: None,
286                    int_vector: None,
287                    logical: false,
288                }
289            }
290            runmat_mir::MirRvalue::Member { base, .. } => self.infer_mir_operand(body, base),
291            runmat_mir::MirRvalue::DynamicMember { base, member } => {
292                self.infer_mir_operand(body, member);
293                self.infer_mir_operand(body, base)
294            }
295            runmat_mir::MirRvalue::Future { args, .. } => {
296                for arg in args {
297                    self.infer_mir_operand(body, arg.operand());
298                }
299                MirShapeValue::default()
300            }
301            runmat_mir::MirRvalue::Spawn(operand) => self.infer_mir_operand(body, operand),
302            runmat_mir::MirRvalue::WorkspaceFirstStaticProperty { .. } => MirShapeValue::default(),
303            runmat_mir::MirRvalue::MetaClass(_)
304            | runmat_mir::MirRvalue::Colon
305            | runmat_mir::MirRvalue::End => MirShapeValue::default(),
306        }
307    }
308
309    fn infer_mir_operand(
310        &self,
311        body: &runmat_mir::MirBody,
312        operand: &runmat_mir::MirOperand,
313    ) -> MirShapeValue {
314        match operand {
315            runmat_mir::MirOperand::Constant(runmat_mir::MirConstant::Number(value)) => {
316                MirShapeValue {
317                    shape: Some(Shape(vec![Some(1), Some(1)])),
318                    number: value.parse().ok(),
319                    int_vector: None,
320                    logical: false,
321                }
322            }
323            runmat_mir::MirOperand::Constant(runmat_mir::MirConstant::Bool(_)) => MirShapeValue {
324                shape: Some(Shape(vec![Some(1), Some(1)])),
325                number: None,
326                int_vector: None,
327                logical: true,
328            },
329            runmat_mir::MirOperand::Local(local) => {
330                let key = MirLocalKey {
331                    function: body.function,
332                    local: *local,
333                };
334                MirShapeValue {
335                    shape: self.local_env.get(&key).cloned(),
336                    number: self.number_env.get(&key).copied(),
337                    int_vector: self.int_vector_env.get(&key).cloned(),
338                    logical: self.logical_env.get(&key).copied().unwrap_or(false),
339                }
340            }
341            runmat_mir::MirOperand::Constant(_) | runmat_mir::MirOperand::FunctionHandle(_) => {
342                MirShapeValue::default()
343            }
344        }
345    }
346
347    fn infer_mir_binary(
348        &mut self,
349        span: Span,
350        lhs: Option<&Shape>,
351        op: &OperatorKind,
352        rhs: Option<&Shape>,
353    ) -> Option<Shape> {
354        match op {
355            OperatorKind::MatrixMultiply => {
356                if let (Some(lhs), Some(rhs)) = (lhs, rhs) {
357                    if matrix_dims(lhs)
358                        .zip(matrix_dims(rhs))
359                        .is_some_and(|((_, lc), (rr, _))| lc.is_some() && rr.is_some() && lc != rr)
360                    {
361                        self.warn(
362                            "lint.shape.matmul",
363                            "matrix multiply inner dimensions must match",
364                            span,
365                        );
366                    }
367                }
368                match (lhs.and_then(matrix_dims), rhs.and_then(matrix_dims)) {
369                    (Some((rows, _)), Some((_, cols))) => Some(Shape(vec![rows, cols])),
370                    _ => None,
371                }
372            }
373            OperatorKind::Add
374            | OperatorKind::Subtract
375            | OperatorKind::ElementwiseMultiply
376            | OperatorKind::ElementwiseDivide
377            | OperatorKind::ElementwiseLeftDivide
378            | OperatorKind::ElementwisePower
379            | OperatorKind::Greater
380            | OperatorKind::GreaterEqual
381            | OperatorKind::Less
382            | OperatorKind::LessEqual
383            | OperatorKind::Equal
384            | OperatorKind::NotEqual
385            | OperatorKind::ElementwiseAnd
386            | OperatorKind::ElementwiseOr => {
387                if let (Some(lhs), Some(rhs)) = (lhs, rhs) {
388                    if !broadcast_compatible(lhs, rhs) {
389                        self.warn(
390                            "lint.shape.broadcast",
391                            "array dimensions are not broadcast compatible",
392                            span,
393                        );
394                    }
395                }
396                lhs.cloned().or_else(|| rhs.cloned())
397            }
398            _ => lhs.cloned().or_else(|| rhs.cloned()),
399        }
400    }
401
402    fn infer_mir_aggregate(
403        &mut self,
404        body: &runmat_mir::MirBody,
405        span: Span,
406        kind: &runmat_mir::MirAggregateKind,
407        rows: usize,
408        elements: &[runmat_mir::MirOperand],
409    ) -> MirShapeValue {
410        let values: Vec<_> = elements
411            .iter()
412            .map(|element| self.infer_mir_operand(body, element))
413            .collect();
414        let int_vector = values
415            .iter()
416            .map(|value| value.number.and_then(number_to_int))
417            .collect::<Option<Vec<_>>>();
418        let shape = match kind {
419            runmat_mir::MirAggregateKind::Tensor => {
420                let row_count = rows.max(1);
421                let cols_per_row = if row_count == 0 {
422                    0
423                } else {
424                    elements.len() / row_count
425                };
426                let mut row_dims = Vec::new();
427                for row_idx in 0..row_count {
428                    let start = row_idx * cols_per_row;
429                    let end = start + cols_per_row;
430                    let mut total_cols = 0usize;
431                    let mut expected_rows = None;
432                    for value in &values[start..end] {
433                        if let Some((rows, cols)) = value.shape.as_ref().and_then(matrix_dims) {
434                            if let (Some(expected), Some(rows)) = (expected_rows, rows) {
435                                if expected != rows {
436                                    self.warn(
437                                        "lint.shape.horzcat",
438                                        "horizontal concatenation row dimensions do not agree",
439                                        span,
440                                    );
441                                }
442                            }
443                            expected_rows = expected_rows.or(rows);
444                            total_cols += cols.unwrap_or(1);
445                        } else {
446                            total_cols += 1;
447                        }
448                    }
449                    row_dims.push((expected_rows.unwrap_or(1), total_cols));
450                }
451                if let Some((_, first_cols)) = row_dims.first().copied() {
452                    for (_, cols) in &row_dims {
453                        if *cols != first_cols {
454                            self.warn(
455                                "lint.shape.vertcat",
456                                "vertical concatenation column dimensions do not agree",
457                                span,
458                            );
459                        }
460                    }
461                    Some(Shape(vec![
462                        Some(row_dims.iter().map(|(rows, _)| rows).sum()),
463                        Some(first_cols),
464                    ]))
465                } else {
466                    Some(Shape(vec![Some(0), Some(0)]))
467                }
468            }
469            runmat_mir::MirAggregateKind::Cell => Some(Shape(vec![Some(1), Some(elements.len())])),
470        };
471        MirShapeValue {
472            shape,
473            number: None,
474            int_vector,
475            logical: false,
476        }
477    }
478
479    fn infer_mir_call(
480        &mut self,
481        body: &runmat_mir::MirBody,
482        span: Span,
483        call: &runmat_mir::MirCall,
484    ) -> MirShapeValue {
485        let arg_values: Vec<_> = call
486            .args
487            .iter()
488            .map(|arg| self.infer_mir_operand(body, arg.operand()))
489            .collect();
490        let shape = match call.semantic_kind {
491            BuiltinSemanticKind::Elementwise => {
492                arg_values.first().and_then(|value| value.shape.clone())
493            }
494            BuiltinSemanticKind::ArrayConstructor => sized_constructor_shape(&arg_values),
495            BuiltinSemanticKind::ParameterizedArrayConstructor => {
496                sized_constructor_shape(arg_values.get(1..).unwrap_or(&[]))
497            }
498            BuiltinSemanticKind::PermutationConstructor => Some(Shape(vec![
499                Some(1),
500                arg_values
501                    .first()
502                    .and_then(|value| value.number.and_then(number_to_int)),
503            ])),
504            BuiltinSemanticKind::RangeConstructor => Some(Shape(vec![Some(1), None])),
505            BuiltinSemanticKind::EmptyConstructor => Some(Shape(vec![Some(0), Some(0)])),
506            BuiltinSemanticKind::ShapeTransform(ShapeTransformKind::Dot) => {
507                let lhs = arg_values.first().and_then(|value| value.shape.as_ref());
508                let rhs = arg_values.get(1).and_then(|value| value.shape.as_ref());
509                if let (Some(lhs), Some(rhs)) = (lhs, rhs) {
510                    if vector_len(lhs)
511                        .zip(vector_len(rhs))
512                        .is_some_and(|(l, r)| l != r)
513                    {
514                        self.warn(
515                            "lint.shape.dot",
516                            "dot product vector lengths do not agree",
517                            span,
518                        );
519                    }
520                }
521                Some(Shape(vec![Some(1), Some(1)]))
522            }
523            BuiltinSemanticKind::ShapeTransform(ShapeTransformKind::Reshape) => {
524                let input = arg_values.first().and_then(|value| value.shape.as_ref());
525                let dims = mir_parse_dims(&arg_values[1..]);
526                if dims.iter().filter(|dim| matches!(dim, Dim::Infer)).count() > 1
527                    || incompatible_element_count(input, &dims)
528                {
529                    self.warn(
530                        "lint.shape.reshape",
531                        "reshape dimensions are not compatible",
532                        span,
533                    );
534                }
535                Some(Shape(dims.iter().map(|dim| dim.as_shape_dim()).collect()))
536            }
537            BuiltinSemanticKind::ShapeTransform(ShapeTransformKind::Repmat) => {
538                for arg in &arg_values[1..] {
539                    if !matches!(mir_parse_dim(arg), Dim::Known(_)) {
540                        self.warn(
541                            "lint.shape.repmat",
542                            "repmat dimensions must be non-negative integers",
543                            span,
544                        );
545                    }
546                }
547                arg_values.first().and_then(|value| value.shape.clone())
548            }
549            BuiltinSemanticKind::ShapeTransform(ShapeTransformKind::Permute) => {
550                let base = arg_values.first().and_then(|value| value.shape.clone());
551                let order = arg_values.get(1).and_then(|value| value.int_vector.clone());
552                if let Some(order) = &order {
553                    let mut sorted = order.clone();
554                    sorted.sort_unstable();
555                    if sorted.windows(2).any(|pair| pair[0] == pair[1])
556                        || base
557                            .as_ref()
558                            .is_some_and(|shape| order.len() != shape.0.len())
559                    {
560                        self.warn(
561                            "lint.shape.permute",
562                            "permute order is invalid for input rank",
563                            span,
564                        );
565                    }
566                }
567                base
568            }
569            BuiltinSemanticKind::ShapeTransform(ShapeTransformKind::Transpose) => {
570                let base = arg_values.first().and_then(|value| value.shape.clone());
571                base.map(|shape| {
572                    if shape.0.len() >= 2 {
573                        Shape(vec![shape.0[1], shape.0[0]])
574                    } else {
575                        shape
576                    }
577                })
578            }
579            BuiltinSemanticKind::ShapeTransform(ShapeTransformKind::Concatenate(kind)) => {
580                self.infer_mir_concat(span, kind, &arg_values)
581            }
582            BuiltinSemanticKind::ShapeTransform(ShapeTransformKind::General) => {
583                arg_values.first().and_then(|value| value.shape.clone())
584            }
585            BuiltinSemanticKind::Reduction => {
586                let base = arg_values.first().and_then(|value| value.shape.clone());
587                if let (Some(base_shape), Some(dim)) = (
588                    base.as_ref(),
589                    arg_values
590                        .get(1)
591                        .and_then(|value| value.number.and_then(number_to_int)),
592                ) {
593                    if dim == 0 || dim > base_shape.0.len() {
594                        self.warn(
595                            "lint.shape.reduction",
596                            "reduction dimension is out of range",
597                            span,
598                        );
599                    }
600                }
601                base
602            }
603            _ => None,
604        };
605        MirShapeValue {
606            shape,
607            number: None,
608            int_vector: None,
609            logical: false,
610        }
611    }
612
613    fn infer_mir_concat(
614        &mut self,
615        span: Span,
616        kind: ConcatKind,
617        arg_values: &[MirShapeValue],
618    ) -> Option<Shape> {
619        let (dim, values) = match kind {
620            ConcatKind::Dimension => {
621                let dim = arg_values
622                    .first()
623                    .and_then(|value| value.number.and_then(number_to_int))?;
624                (dim, &arg_values[1..])
625            }
626            ConcatKind::Horizontal => (2, arg_values),
627            ConcatKind::Vertical => (1, arg_values),
628        };
629        let shapes: Vec<_> = values
630            .iter()
631            .filter_map(|value| value.shape.as_ref())
632            .collect();
633        if shapes.is_empty() || dim == 0 {
634            return None;
635        }
636        let rank = shapes
637            .iter()
638            .map(|shape| shape.0.len())
639            .max()
640            .unwrap_or(dim);
641        let axis = dim - 1;
642        if axis >= rank {
643            return None;
644        }
645        let mut out = vec![Some(1); rank];
646        for (idx, out_dim) in out.iter_mut().enumerate().take(rank) {
647            if idx == axis {
648                *out_dim = shapes
649                    .iter()
650                    .map(|shape| shape.0.get(idx).copied().flatten())
651                    .try_fold(0usize, |sum, dim| dim.map(|dim| sum + dim));
652                continue;
653            }
654            let mut expected = None;
655            for shape in &shapes {
656                let dim = shape.0.get(idx).copied().flatten().or(Some(1));
657                if let (Some(expected), Some(dim)) = (expected, dim) {
658                    if expected != dim {
659                        self.warn(
660                            "lint.shape.concat",
661                            "concatenation dimensions do not agree",
662                            span,
663                        );
664                    }
665                }
666                expected = expected.or(dim);
667            }
668            *out_dim = expected;
669        }
670        Some(Shape(out))
671    }
672
673    fn check_logical_index(&mut self, span: Span, base: Option<&Shape>, idx: Option<&Shape>) {
674        if let (Some(base), Some(idx)) = (base, idx) {
675            if element_count(base)
676                .zip(element_count(idx))
677                .is_some_and(|(base, idx)| base != idx)
678            {
679                self.warn(
680                    "lint.shape.logical_index",
681                    "logical index shape does not match indexed value",
682                    span,
683                );
684            }
685        }
686    }
687
688    fn warn(&mut self, code: &'static str, message: &'static str, span: Span) {
689        self.diagnostics.push(
690            HirDiagnostic::new(code, HirDiagnosticSeverity::Warning, message, span)
691                .with_category("shape"),
692        );
693    }
694}
695
696#[derive(Clone, Copy, PartialEq)]
697enum Dim {
698    Known(usize),
699    Infer,
700    Unknown,
701}
702
703impl Dim {
704    fn as_shape_dim(self) -> Option<usize> {
705        match self {
706            Dim::Known(value) => Some(value),
707            Dim::Infer | Dim::Unknown => None,
708        }
709    }
710}
711
712fn number_to_int(value: f64) -> Option<usize> {
713    if value.is_finite() && value >= 0.0 && (value.fract().abs() <= 1e-9) {
714        Some(value as usize)
715    } else {
716        None
717    }
718}
719
720fn is_logical_operator(op: &OperatorKind) -> bool {
721    matches!(
722        op,
723        OperatorKind::Not
724            | OperatorKind::Greater
725            | OperatorKind::GreaterEqual
726            | OperatorKind::Less
727            | OperatorKind::LessEqual
728            | OperatorKind::Equal
729            | OperatorKind::NotEqual
730            | OperatorKind::ShortCircuitAnd
731            | OperatorKind::ShortCircuitOr
732            | OperatorKind::ElementwiseAnd
733            | OperatorKind::ElementwiseOr
734    )
735}
736
737fn mir_parse_dim(value: &MirShapeValue) -> Dim {
738    match value.number {
739        Some(-1.0) => Dim::Infer,
740        Some(value) => number_to_int(value).map(Dim::Known).unwrap_or(Dim::Unknown),
741        None => Dim::Unknown,
742    }
743}
744
745fn mir_parse_dims(args: &[MirShapeValue]) -> Vec<Dim> {
746    if args.len() == 1 {
747        if let Some(values) = &args[0].int_vector {
748            return values.iter().copied().map(Dim::Known).collect();
749        }
750    }
751    args.iter().map(mir_parse_dim).collect()
752}
753
754fn sized_constructor_shape(args: &[MirShapeValue]) -> Option<Shape> {
755    let dims: Vec<_> = args
756        .iter()
757        .filter_map(|value| value.number.and_then(number_to_int))
758        .map(Some)
759        .collect();
760    match dims.as_slice() {
761        [] => None,
762        [dim] => Some(Shape(vec![*dim, *dim])),
763        _ => Some(Shape(dims)),
764    }
765}
766
767fn shape_from_fact(shape: &runmat_hir::ShapeFact) -> Option<Shape> {
768    match shape {
769        runmat_hir::ShapeFact::Scalar => Some(Shape(vec![Some(1), Some(1)])),
770        runmat_hir::ShapeFact::Shaped { dims } => Some(Shape(
771            dims.iter()
772                .map(|dim| match dim {
773                    runmat_hir::DimFact::Known(value) => Some(*value),
774                    runmat_hir::DimFact::Symbolic(_) | runmat_hir::DimFact::Unknown => None,
775                })
776                .collect(),
777        )),
778        runmat_hir::ShapeFact::Ranked { .. }
779        | runmat_hir::ShapeFact::Unknown
780        | runmat_hir::ShapeFact::Unreachable => None,
781    }
782}
783
784fn range_width(start: f64, step: f64, end: f64) -> Option<usize> {
785    if step == 0.0 || !start.is_finite() || !step.is_finite() || !end.is_finite() {
786        return None;
787    }
788    let span = end - start;
789    if (span > 0.0 && step < 0.0) || (span < 0.0 && step > 0.0) {
790        return Some(0);
791    }
792    Some((span / step).floor().abs() as usize + 1)
793}
794
795fn matrix_dims(shape: &Shape) -> Option<(Option<usize>, Option<usize>)> {
796    Some((*shape.0.first()?, *shape.0.get(1)?))
797}
798
799fn element_count(shape: &Shape) -> Option<usize> {
800    shape
801        .0
802        .iter()
803        .try_fold(1usize, |acc, dim| dim.map(|dim| acc * dim))
804}
805
806fn vector_len(shape: &Shape) -> Option<usize> {
807    let count = element_count(shape)?;
808    if shape.0.len() == 1
809        || (shape.0.len() == 2 && (shape.0[0] == Some(1) || shape.0[1] == Some(1)))
810    {
811        Some(count)
812    } else {
813        None
814    }
815}
816
817fn infer_mir_index_shape(
818    base: Option<&Shape>,
819    indexing: &runmat_mir::MirIndexing,
820    component_values: &[Option<MirShapeValue>],
821) -> Option<Shape> {
822    if indexing.kind != IndexKind::Paren {
823        return None;
824    }
825    if indexing.components.is_empty() {
826        return base.cloned();
827    }
828    if indexing.plan == runmat_mir::MirIndexPlan::Scalar {
829        return Some(Shape(vec![Some(1), Some(1)]));
830    }
831    if indexing.components.len() == 1 {
832        return match indexing.components.first()? {
833            runmat_mir::MirIndexComponent::Colon => base.cloned(),
834            runmat_mir::MirIndexComponent::End { .. } => Some(Shape(vec![Some(1), Some(1)])),
835            runmat_mir::MirIndexComponent::Expr(_) => {
836                let value = component_values.first()?.as_ref()?;
837                if value.logical {
838                    None
839                } else if value.number.is_some() {
840                    Some(Shape(vec![Some(1), Some(1)]))
841                } else {
842                    value.shape.clone()
843                }
844            }
845        };
846    }
847
848    let dims = indexing
849        .components
850        .iter()
851        .enumerate()
852        .map(|(idx, component)| match component {
853            runmat_mir::MirIndexComponent::Colon => {
854                base.and_then(|shape| shape.0.get(idx).copied().flatten())
855            }
856            runmat_mir::MirIndexComponent::End { .. } => Some(1),
857            runmat_mir::MirIndexComponent::Expr(_) => component_values
858                .get(idx)
859                .and_then(|value| value.as_ref())
860                .and_then(numeric_index_component_len),
861        })
862        .collect();
863    Some(Shape(dims))
864}
865
866fn numeric_index_component_len(value: &MirShapeValue) -> Option<usize> {
867    if value.logical {
868        return None;
869    }
870    if value.number.is_some() {
871        return Some(1);
872    }
873    value.shape.as_ref().and_then(vector_len)
874}
875
876fn broadcast_compatible(left: &Shape, right: &Shape) -> bool {
877    let len = left.0.len().max(right.0.len());
878    (0..len).all(|idx| {
879        let l = left.0.iter().rev().nth(idx).copied().flatten().unwrap_or(1);
880        let r = right
881            .0
882            .iter()
883            .rev()
884            .nth(idx)
885            .copied()
886            .flatten()
887            .unwrap_or(1);
888        l == r || l == 1 || r == 1
889    })
890}
891
892fn incompatible_element_count(input: Option<&Shape>, dims: &[Dim]) -> bool {
893    let Some(input_count) = input.and_then(element_count) else {
894        return false;
895    };
896    if dims.iter().any(|dim| matches!(dim, Dim::Unknown)) {
897        return true;
898    }
899    let known_product = dims.iter().fold(1usize, |acc, dim| match dim {
900        Dim::Known(value) => acc * value,
901        Dim::Infer | Dim::Unknown => acc,
902    });
903    if dims.iter().any(|dim| matches!(dim, Dim::Infer)) {
904        known_product == 0 || input_count % known_product != 0
905    } else {
906        known_product != input_count
907    }
908}