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}