1use crate::builtins::common::tensor::{
2 self, complex_tensor_element_len, complex_tensor_values_complex64, is_scalar_tensor,
3 tensor_element_len, tensor_value_f64,
4};
5use crate::indexing::integer_assignment::{
6 self, ComplexIntegerAssignmentValue, IntegerAssignmentValue,
7};
8use crate::indexing::plan::IndexPlan;
9use crate::runtime_error::semantic_error as mex;
10use crate::RuntimeError;
11use runmat_accelerate_api::{HostIntegerDataOwned, HostIntegerDataView, HostIntegerTensorView};
12use runmat_value::{
13 ComplexTensor, IntegerComplexStorage, IntegerStorage, NumericDType, NumericScalar,
14 NumericStorage, SparseTensor, StringArray, Tensor, Value,
15};
16
17fn map_slice_shape_error(context: &str, err: impl std::fmt::Display) -> RuntimeError {
18 mex("ShapeMismatch", format!("{context}: {err}"))
19}
20
21fn map_acceleration_error(context: &str, err: impl std::fmt::Display) -> RuntimeError {
22 mex("AccelerationOperationFailed", format!("{context}: {err}"))
23}
24
25fn is_empty_delete_rhs(value: &Value) -> bool {
26 matches!(
27 value,
28 Value::Tensor(t)
29 if tensor_element_len(t) == 0 || t.rows == 0 || t.cols == 0
30 ) || matches!(
31 value,
32 Value::ComplexTensor(t)
33 if complex_tensor_element_len(t) == 0 || t.rows == 0 || t.cols == 0
34 ) || matches!(value, Value::OutputList(values) if values.is_empty())
35}
36
37pub(crate) fn real_tensor_to_complex(
38 tensor: Tensor,
39 context: &str,
40) -> Result<ComplexTensor, RuntimeError> {
41 let shape = tensor.shape.clone();
42 let storage = tensor
43 .into_numeric_storage()
44 .map_err(|error| map_slice_shape_error(context, error))?;
45 match storage.into_integer_storage() {
46 Ok(real) => {
47 let imag = real.zeros_like(real.len());
48 let storage = IntegerComplexStorage::new(real, imag)
49 .expect("same-class real and imaginary integer storage must be valid");
50 ComplexTensor::new_integer(storage, shape)
51 .map_err(|error| map_slice_shape_error(context, error))
52 }
53 Err(storage) => ComplexTensor::new(
54 storage
55 .materialize_f64()
56 .into_iter()
57 .map(|real| (real, 0.0))
58 .collect(),
59 shape,
60 )
61 .map_err(|error| map_slice_shape_error(context, error)),
62 }
63}
64
65pub(crate) fn deleted_vector_shape(rows: usize, _cols: usize, len: usize) -> Vec<usize> {
66 if len == 0 {
67 vec![0, 0]
68 } else if rows == 1 {
69 vec![1, len]
70 } else {
71 vec![len, 1]
72 }
73}
74
75fn sorted_unique_positions_desc(
76 plan: &IndexPlan,
77 total: usize,
78) -> Result<Vec<usize>, RuntimeError> {
79 let mut positions = Vec::with_capacity(plan.indices.len());
80 for &idx in &plan.indices {
81 let pos = idx as usize;
82 if pos >= total {
83 return Err(mex("IndexOutOfBounds", "Index out of bounds"));
84 }
85 positions.push(pos);
86 }
87 positions.sort_unstable();
88 positions.dedup();
89 positions.reverse();
90 Ok(positions)
91}
92
93fn delete_integer_storage_positions(
94 storage: IntegerStorage,
95 positions: &[usize],
96) -> IntegerStorage {
97 macro_rules! delete_positions {
98 ($values:expr, $variant:ident) => {{
99 let mut values = $values;
100 for &position in positions {
101 values.remove(position);
102 }
103 IntegerStorage::$variant(values)
104 }};
105 }
106
107 match storage {
108 IntegerStorage::I8(values) => delete_positions!(values, I8),
109 IntegerStorage::I16(values) => delete_positions!(values, I16),
110 IntegerStorage::I32(values) => delete_positions!(values, I32),
111 IntegerStorage::I64(values) => delete_positions!(values, I64),
112 IntegerStorage::U8(values) => delete_positions!(values, U8),
113 IntegerStorage::U16(values) => delete_positions!(values, U16),
114 IntegerStorage::U32(values) => delete_positions!(values, U32),
115 IntegerStorage::U64(values) => delete_positions!(values, U64),
116 }
117}
118
119pub(crate) fn delete_integer_complex_storage_positions(
120 storage: IntegerComplexStorage,
121 positions: &[usize],
122) -> IntegerComplexStorage {
123 IntegerComplexStorage::new(
124 delete_integer_storage_positions(storage.real, positions),
125 delete_integer_storage_positions(storage.imag, positions),
126 )
127 .expect("paired integer complex storage must retain matching classes and lengths")
128}
129
130fn scalar_integer_value(value: &Value) -> Result<IntegerAssignmentValue, RuntimeError> {
131 match value {
132 Value::Int(value) => Ok(IntegerAssignmentValue::Exact(value.clone())),
133 Value::Num(value) => Ok(IntegerAssignmentValue::Float(*value)),
134 Value::Bool(value) => Ok(IntegerAssignmentValue::Float(if *value {
135 1.0
136 } else {
137 0.0
138 })),
139 Value::Tensor(tensor) if is_scalar_tensor(tensor) => {
140 let value = tensor
141 .numeric_value_at(0)
142 .expect("scalar tensor must contain one numeric value");
143 Ok(match value.into_int_value() {
144 Some(value) => IntegerAssignmentValue::Exact(value),
145 None => IntegerAssignmentValue::Float(value.materialize_f64()),
146 })
147 }
148 Value::LogicalArray(array) if array.data.len() == 1 => {
149 Ok(IntegerAssignmentValue::Float(if array.data[0] == 0 {
150 0.0
151 } else {
152 1.0
153 }))
154 }
155 _ => Err(mex(
156 "InvalidSliceAssignmentRhs",
157 "rhs must be numeric or logical",
158 )),
159 }
160}
161
162fn validated_assignment_rhs_shape(
163 rhs_shape: &[usize],
164 rhs_len: usize,
165 selection_lengths: &[usize],
166) -> Result<Vec<usize>, RuntimeError> {
167 let dims = selection_lengths.len();
168 if rhs_len == 1 {
169 return Ok(vec![1; dims]);
170 }
171 if dims == 1 {
172 if rhs_len == selection_lengths[0] {
173 return Ok(vec![rhs_len]);
174 }
175 return Err(mex("ShapeMismatch", "shape mismatch for slice assign"));
176 }
177 if rhs_shape.len() > dims && rhs_shape.iter().skip(dims).any(|&dimension| dimension != 1) {
178 return Err(mex("ShapeMismatch", "shape mismatch for slice assign"));
179 }
180 let mut shape = rhs_shape.iter().copied().take(dims).collect::<Vec<_>>();
181 shape.resize(dims, 1);
182 if shape != selection_lengths {
183 return Err(mex("ShapeMismatch", "shape mismatch for slice assign"));
184 }
185 let expected = shape
186 .iter()
187 .try_fold(1usize, |length, dimension| length.checked_mul(*dimension))
188 .ok_or_else(|| mex("ShapeMismatch", "shape mismatch for slice assign"))?;
189 if rhs_len != expected {
190 return Err(mex("ShapeMismatch", "shape mismatch for slice assign"));
191 }
192 Ok(shape)
193}
194
195pub enum ComplexRhsView {
196 Scalar((f64, f64)),
197 Tensor {
198 data: Vec<(f64, f64)>,
199 shape: Vec<usize>,
200 strides: Vec<usize>,
201 },
202}
203
204pub fn build_complex_rhs_view(
205 rhs: &Value,
206 selection_lengths: &[usize],
207) -> Result<ComplexRhsView, RuntimeError> {
208 match rhs {
209 Value::Complex(re, im) => Ok(ComplexRhsView::Scalar((*re, *im))),
210 Value::Num(n) => Ok(ComplexRhsView::Scalar((*n, 0.0))),
211 Value::ComplexTensor(rt) => {
212 let dims = selection_lengths.len();
213 let shape = validated_assignment_rhs_shape(&rt.shape, rt.len(), selection_lengths)?;
214 let mut rstrides = vec![0usize; dims];
215 let mut racc = 1usize;
216 for d in 0..dims {
217 rstrides[d] = racc;
218 racc *= shape[d];
219 }
220 Ok(ComplexRhsView::Tensor {
221 data: complex_tensor_values_complex64(rt)
222 .into_iter()
223 .map(|value| (value.re, value.im))
224 .collect(),
225 shape,
226 strides: rstrides,
227 })
228 }
229 _ => Err(mex(
230 "InvalidSliceAssignmentRhs",
231 "rhs must be numeric or tensor",
232 )),
233 }
234}
235
236pub fn scatter_complex_with_plan(
237 t: &mut ComplexTensor,
238 plan: &IndexPlan,
239 rhs_view: &ComplexRhsView,
240) -> Result<(), RuntimeError> {
241 let dims = plan.dims;
242 let mut idx = vec![0usize; dims];
243 if plan.indices.is_empty() {
244 return Ok(());
245 }
246 let selection_lengths = if plan.selection_lengths.is_empty() {
247 plan.output_shape.clone()
248 } else {
249 plan.selection_lengths.clone()
250 };
251 loop {
252 let mut rlin = 0usize;
253 match rhs_view {
254 ComplexRhsView::Scalar(val) => {
255 let lin_pos = {
256 let mut p = 0usize;
257 let mut mul = 1usize;
258 for d in 0..dims {
259 p += idx[d] * mul;
260 mul *= selection_lengths[d].max(1);
261 }
262 p
263 };
264 let dst = plan.indices[lin_pos] as usize;
265 t.set_f64_assignment_at(dst, val.0, val.1)
266 .map_err(|error| map_slice_shape_error("complex slice assign", error))?;
267 }
268 ComplexRhsView::Tensor {
269 data,
270 shape,
271 strides,
272 } => {
273 for d in 0..dims {
274 let rhs_len = shape[d];
275 let pos = if rhs_len == 1 { 0 } else { idx[d] };
276 rlin += pos * strides[d];
277 }
278 let lin_pos = {
279 let mut p = 0usize;
280 let mut mul = 1usize;
281 for d in 0..dims {
282 p += idx[d] * mul;
283 mul *= selection_lengths[d].max(1);
284 }
285 p
286 };
287 let dst = plan.indices[lin_pos] as usize;
288 t.set_f64_assignment_at(dst, data[rlin].0, data[rlin].1)
289 .map_err(|error| map_slice_shape_error("complex slice assign", error))?;
290 }
291 }
292 let mut d = 0usize;
293 while d < dims {
294 idx[d] += 1;
295 if idx[d] < selection_lengths[d].max(1) {
296 break;
297 }
298 idx[d] = 0;
299 d += 1;
300 }
301 if d == dims {
302 break;
303 }
304 }
305 Ok(())
306}
307
308pub enum StringRhsView {
309 Scalar(String),
310 Tensor {
311 data: Vec<String>,
312 shape: Vec<usize>,
313 strides: Vec<usize>,
314 },
315}
316
317pub fn build_string_rhs_view(
318 rhs: &Value,
319 selection_lengths: &[usize],
320) -> Result<StringRhsView, RuntimeError> {
321 let scalar = match rhs {
322 Value::String(s) => Some(s.clone()),
323 Value::CharArray(chars) => Some(chars.row_string().ok_or_else(|| {
324 mex(
325 "InvalidSliceAssignmentRhs",
326 "rhs character array must be a row vector",
327 )
328 })?),
329 _ => None,
330 };
331 if let Some(s) = scalar {
332 return Ok(StringRhsView::Scalar(s));
333 }
334 if let Value::StringArray(rt) = rhs {
335 let dims = selection_lengths.len();
336 let shape = validated_assignment_rhs_shape(&rt.shape, rt.data.len(), selection_lengths)?;
337 let mut rstrides = vec![0usize; dims];
338 let mut racc = 1usize;
339 for d in 0..dims {
340 rstrides[d] = racc;
341 racc *= shape[d];
342 }
343 return Ok(StringRhsView::Tensor {
344 data: rt.data.clone(),
345 shape,
346 strides: rstrides,
347 });
348 }
349 if let Value::Cell(cell) = rhs {
350 let dims = selection_lengths.len();
351 let mut data = Vec::with_capacity(cell.data.len());
352 for handle in &cell.data {
353 let value = handle;
354 match value {
355 Value::String(text) => data.push(text.clone()),
356 Value::CharArray(chars) => data.push(chars.row_string().ok_or_else(|| {
357 mex(
358 "InvalidSliceAssignmentRhs",
359 "rhs cell character arrays must be row vectors",
360 )
361 })?),
362 Value::StringArray(strings) if strings.data.len() == 1 => {
363 data.push(strings.data[0].clone())
364 }
365 other => {
366 return Err(mex(
367 "InvalidSliceAssignmentRhs",
368 format!(
369 "rhs cell elements must be string scalars or character vectors, got {other:?}"
370 ),
371 ))
372 }
373 }
374 }
375 let shape =
376 validated_assignment_rhs_shape(&cell.shape, cell.data.len(), selection_lengths)?;
377 let mut rstrides = vec![0usize; dims];
378 let mut racc = 1usize;
379 for d in 0..dims {
380 rstrides[d] = racc;
381 racc *= shape[d];
382 }
383 return Ok(StringRhsView::Tensor {
384 data,
385 shape,
386 strides: rstrides,
387 });
388 }
389 Err(mex(
390 "InvalidSliceAssignmentRhs",
391 "rhs must be string, string array, or cellstr",
392 ))
393}
394
395pub fn scatter_string_with_plan(
396 sa: &mut StringArray,
397 plan: &IndexPlan,
398 rhs_view: &StringRhsView,
399) -> Result<(), RuntimeError> {
400 let dims = plan.dims;
401 let mut idx = vec![0usize; dims];
402 if plan.indices.is_empty() {
403 return Ok(());
404 }
405 let selection_lengths = if plan.selection_lengths.is_empty() {
406 plan.output_shape.clone()
407 } else {
408 plan.selection_lengths.clone()
409 };
410 loop {
411 match rhs_view {
412 StringRhsView::Scalar(val) => {
413 let lin_pos = {
414 let mut p = 0usize;
415 let mut mul = 1usize;
416 for d in 0..dims {
417 p += idx[d] * mul;
418 mul *= selection_lengths[d].max(1);
419 }
420 p
421 };
422 let dst = plan.indices[lin_pos] as usize;
423 sa.data[dst] = val.clone();
424 }
425 StringRhsView::Tensor {
426 data,
427 shape,
428 strides,
429 } => {
430 let mut rlin = 0usize;
431 for d in 0..dims {
432 let rhs_len = shape[d];
433 let pos = if rhs_len == 1 { 0 } else { idx[d] };
434 rlin += pos * strides[d];
435 }
436 let lin_pos = {
437 let mut p = 0usize;
438 let mut mul = 1usize;
439 for d in 0..dims {
440 p += idx[d] * mul;
441 mul *= selection_lengths[d].max(1);
442 }
443 p
444 };
445 let dst = plan.indices[lin_pos] as usize;
446 sa.data[dst] = data[rlin].clone();
447 }
448 }
449 let mut d = 0usize;
450 while d < dims {
451 idx[d] += 1;
452 if idx[d] < selection_lengths[d].max(1) {
453 break;
454 }
455 idx[d] = 0;
456 d += 1;
457 }
458 if d == dims {
459 break;
460 }
461 }
462 Ok(())
463}
464
465pub async fn materialize_rhs_real_for_plan(
466 rhs: &Value,
467 plan: &IndexPlan,
468) -> Result<Vec<f64>, RuntimeError> {
469 if plan.dims == 1 {
470 let count = plan.selection_lengths.first().copied().unwrap_or(0);
471 materialize_rhs_linear_real(rhs, count).await
472 } else {
473 materialize_rhs_nd_real(rhs, &plan.selection_lengths).await
474 }
475}
476
477pub(crate) async fn materialize_integer_rhs_for_plan(
478 rhs: &Value,
479 plan: &IndexPlan,
480) -> Result<Vec<IntegerAssignmentValue>, RuntimeError> {
481 match rhs {
482 Value::Int(value) => Ok(vec![
483 IntegerAssignmentValue::Exact(value.clone());
484 plan.indices.len()
485 ]),
486 Value::Tensor(tensor) if tensor.integer_storage().is_some() => {
487 materialize_integer_tensor_rhs_for_plan(tensor, plan)
488 }
489 Value::GpuTensor(handle)
490 if runmat_accelerate_api::handle_integer_type(handle).is_some() =>
491 {
492 let tensor = download_integer_tensor(handle).await?;
493 materialize_integer_tensor_rhs_for_plan(&tensor, plan)
494 }
495 Value::OutputList(values) => {
496 if values.len() == plan.indices.len() {
497 return values.iter().map(scalar_integer_value).collect();
498 }
499 if values.len() == 1 {
500 return Ok(vec![scalar_integer_value(&values[0])?; plan.indices.len()]);
501 }
502 Err(mex("ShapeMismatch", "shape mismatch for slice assign"))
503 }
504 _ => materialize_rhs_real_for_plan(rhs, plan)
505 .await
506 .map(|values| {
507 values
508 .into_iter()
509 .map(IntegerAssignmentValue::Float)
510 .collect()
511 }),
512 }
513}
514
515fn materialize_integer_tensor_rhs_for_plan(
516 tensor: &Tensor,
517 plan: &IndexPlan,
518) -> Result<Vec<IntegerAssignmentValue>, RuntimeError> {
519 let values = integer_assignment::values(
520 tensor
521 .integer_storage()
522 .expect("integer RHS must retain exact storage"),
523 );
524 integer_gpu_rhs_indices_for_plan(&tensor.shape, plan)?
525 .into_iter()
526 .map(|index| {
527 values
528 .get(index as usize)
529 .cloned()
530 .map(IntegerAssignmentValue::Exact)
531 .ok_or_else(|| mex("ShapeMismatch", "shape mismatch for slice assign"))
532 })
533 .collect()
534}
535
536async fn materialize_complex_integer_rhs_for_plan(
537 rhs: &Value,
538 plan: &IndexPlan,
539) -> Result<Vec<ComplexIntegerAssignmentValue>, RuntimeError> {
540 match rhs {
541 Value::Complex(real, imag) => Ok(vec![
542 ComplexIntegerAssignmentValue {
543 real: IntegerAssignmentValue::Float(*real),
544 imag: IntegerAssignmentValue::Float(*imag),
545 };
546 plan.indices.len()
547 ]),
548 Value::ComplexTensor(tensor) => {
549 let values = if let Some(storage) = &tensor.integer_storage() {
550 (0..storage.len())
551 .map(|index| ComplexIntegerAssignmentValue {
552 real: IntegerAssignmentValue::Exact(
553 storage
554 .real
555 .value_at(index)
556 .expect("typed complex storage length was validated"),
557 ),
558 imag: IntegerAssignmentValue::Exact(
559 storage
560 .imag
561 .value_at(index)
562 .expect("typed complex storage length was validated"),
563 ),
564 })
565 .collect()
566 } else {
567 tensor
568 .materialize_f64()
569 .iter()
570 .map(|&(real, imag)| ComplexIntegerAssignmentValue {
571 real: IntegerAssignmentValue::Float(real),
572 imag: IntegerAssignmentValue::Float(imag),
573 })
574 .collect()
575 };
576 materialize_complex_integer_values_for_plan(values, &tensor.shape, plan)
577 }
578 _ => materialize_integer_rhs_for_plan(rhs, plan)
579 .await
580 .map(|values| {
581 values
582 .into_iter()
583 .map(|real| ComplexIntegerAssignmentValue {
584 real,
585 imag: IntegerAssignmentValue::Float(0.0),
586 })
587 .collect()
588 }),
589 }
590}
591
592fn materialize_complex_integer_values_for_plan(
593 values: Vec<ComplexIntegerAssignmentValue>,
594 rhs_shape: &[usize],
595 plan: &IndexPlan,
596) -> Result<Vec<ComplexIntegerAssignmentValue>, RuntimeError> {
597 if plan.dims == 1 {
598 if values.len() == plan.indices.len() {
599 return Ok(values);
600 }
601 if values.len() == 1 {
602 return Ok(vec![values[0].clone(); plan.indices.len()]);
603 }
604 return Err(mex("ShapeMismatch", "shape mismatch for slice assign"));
605 }
606
607 let dims = plan.selection_lengths.len();
608 let shape = validated_assignment_rhs_shape(rhs_shape, values.len(), &plan.selection_lengths)?;
609 let mut strides = vec![1usize; dims];
610 for dimension in 1..dims {
611 strides[dimension] = strides[dimension - 1] * shape[dimension - 1].max(1);
612 }
613 let mut output = Vec::with_capacity(plan.indices.len());
614 let mut coordinates = vec![0usize; dims];
615 for _ in 0..plan.indices.len() {
616 let mut rhs_index = 0usize;
617 for dimension in 0..dims {
618 let coordinate = if shape[dimension] == 1 {
619 0
620 } else {
621 coordinates[dimension]
622 };
623 rhs_index += coordinate * strides[dimension];
624 }
625 output.push(values[rhs_index].clone());
626 for (dimension, coordinate) in coordinates.iter_mut().enumerate() {
627 *coordinate += 1;
628 if *coordinate < plan.selection_lengths[dimension].max(1) {
629 break;
630 }
631 *coordinate = 0;
632 }
633 }
634 Ok(output)
635}
636
637pub fn scatter_real_with_plan(
638 storage: &mut NumericStorage,
639 plan: &IndexPlan,
640 rhs_values: &[f64],
641) -> Result<(), RuntimeError> {
642 if rhs_values.len() != plan.indices.len() {
643 return Err(mex("ShapeMismatch", "shape mismatch for slice assign"));
644 }
645 for (&dst, &value) in plan.indices.iter().zip(rhs_values.iter()) {
646 let value = match storage.numeric_dtype() {
647 NumericDType::F64 => NumericScalar::F64(value),
648 NumericDType::F32 => NumericScalar::F32(value as f32),
649 _ => unreachable!("real floating scatter requires floating storage"),
650 };
651 storage
652 .set_value(dst as usize, value)
653 .map_err(|error| map_slice_shape_error("slice assign", error))?;
654 }
655 Ok(())
656}
657
658pub async fn assign_sparse_with_plan(
662 sparse: SparseTensor,
663 plan: &IndexPlan,
664 rhs: &Value,
665) -> Result<Value, RuntimeError> {
666 if sparse.integer_storage().is_some() {
667 crate::compatibility::ensure_sparse_integer_extension_enabled("indexed assignment")?;
668 }
669 if is_empty_delete_rhs(rhs) {
670 return Err(mex(
671 "SparseAssignmentUnsupported",
672 "Sparse indexed deletion is not yet supported",
673 ));
674 }
675 let target_rows = plan.base_shape.first().copied().unwrap_or(sparse.rows);
676 let target_cols = plan.base_shape.get(1).copied().unwrap_or(sparse.cols);
677 let sparse = if target_rows != sparse.rows || target_cols != sparse.cols {
678 sparse
679 .with_expanded_shape(target_rows, target_cols)
680 .map_err(|error| map_slice_shape_error("sparse slice expansion", error))?
681 } else {
682 sparse
683 };
684 if plan.indices.is_empty() {
685 return Ok(Value::SparseTensor(sparse));
686 }
687 let updated = if sparse.is_logical() {
688 let rhs_values = materialize_rhs_real_for_plan(rhs, plan).await?;
689 let updates = plan
690 .indices
691 .iter()
692 .zip(rhs_values)
693 .map(|(&index, value)| (index as usize, value != 0.0))
694 .collect::<Vec<_>>();
695 sparse
696 .with_updated_logical_linear_values(&updates)
697 .map_err(|error| map_slice_shape_error("sparse slice assign", error))?
698 } else if let Some(storage) = sparse.integer_storage() {
699 let rhs_values = materialize_integer_rhs_for_plan(rhs, plan).await?;
700 let updates = plan
701 .indices
702 .iter()
703 .zip(rhs_values.iter())
704 .map(|(&index, value)| {
705 (
706 index as usize,
707 integer_assignment::scalar_value(storage, value),
708 )
709 })
710 .collect::<Vec<_>>();
711 sparse
712 .with_updated_integer_linear_values(&updates)
713 .map_err(|error| map_slice_shape_error("sparse slice assign", error))?
714 } else if sparse.numeric_dtype() == Some(NumericDType::F32) {
715 let rhs_values = materialize_rhs_real_for_plan(rhs, plan).await?;
716 let updates = plan
717 .indices
718 .iter()
719 .zip(rhs_values)
720 .map(|(&index, value)| (index as usize, value as f32))
721 .collect::<Vec<_>>();
722 sparse
723 .with_updated_f32_linear_values(&updates)
724 .map_err(|error| map_slice_shape_error("sparse slice assign", error))?
725 } else {
726 let rhs_values = materialize_rhs_real_for_plan(rhs, plan).await?;
727 let updates = plan
728 .indices
729 .iter()
730 .zip(rhs_values)
731 .map(|(&index, value)| (index as usize, value))
732 .collect::<Vec<_>>();
733 sparse
734 .with_updated_linear_values(&updates)
735 .map_err(|error| map_slice_shape_error("sparse slice assign", error))?
736 };
737 Ok(Value::SparseTensor(updated))
738}
739
740enum SparseDeletionAxis {
741 Rows(Vec<usize>),
742 Columns(Vec<usize>),
743 All,
744}
745
746fn sparse_deletion_axis(
747 sparse: &SparseTensor,
748 plan: &IndexPlan,
749) -> Result<SparseDeletionAxis, RuntimeError> {
750 if plan.indices.is_empty() {
751 return Ok(SparseDeletionAxis::Rows(Vec::new()));
752 }
753 if plan.dims == 1 {
754 let indices = plan.indices.iter().map(|&index| index as usize).collect();
755 if sparse.rows == 1 {
756 return Ok(SparseDeletionAxis::Columns(indices));
757 }
758 if sparse.cols == 1 {
759 return Ok(SparseDeletionAxis::Rows(indices));
760 }
761 return Err(mex(
762 "UnsupportedDeletion",
763 "Linear sparse deletion is only supported for vectors",
764 ));
765 }
766 if plan.dims != 2 {
767 return Err(mex(
768 "UnsupportedDeletion",
769 "Sparse deletion currently supports vectors and complete matrix rows or columns",
770 ));
771 }
772 let selected_rows = plan.selection_lengths.first().copied().unwrap_or(0);
773 let selected_cols = plan.selection_lengths.get(1).copied().unwrap_or(0);
774 if selected_rows == sparse.rows && selected_cols == sparse.cols {
775 return Ok(SparseDeletionAxis::All);
776 }
777 if selected_rows == sparse.rows {
778 let columns = plan
779 .indices
780 .chunks(sparse.rows)
781 .map(|chunk| chunk[0] as usize / sparse.rows)
782 .collect();
783 return Ok(SparseDeletionAxis::Columns(columns));
784 }
785 if selected_cols == sparse.cols {
786 let rows = plan
787 .indices
788 .iter()
789 .take(selected_rows)
790 .map(|index| *index as usize % sparse.rows)
791 .collect();
792 return Ok(SparseDeletionAxis::Rows(rows));
793 }
794 Err(mex(
795 "UnsupportedDeletion",
796 "Sparse deletion requires selecting complete rows or columns",
797 ))
798}
799
800pub fn delete_sparse_with_plan(
801 sparse: SparseTensor,
802 plan: &IndexPlan,
803 rhs: &Value,
804) -> Result<Value, RuntimeError> {
805 if sparse.integer_storage().is_some() {
806 crate::compatibility::ensure_sparse_integer_extension_enabled("indexed assignment")?;
807 }
808 if !is_empty_delete_rhs(rhs) {
809 return Err(mex(
810 "DeletionRequiresEmptyRhs",
811 "Indexed deletion requires empty RHS",
812 ));
813 }
814 let updated = match sparse_deletion_axis(&sparse, plan)? {
815 SparseDeletionAxis::Rows(rows) => sparse.with_deleted_rows(&rows),
816 SparseDeletionAxis::Columns(columns) => sparse.with_deleted_columns(&columns),
817 SparseDeletionAxis::All => {
818 let rows = (0..sparse.rows).collect::<Vec<_>>();
819 let columns = (0..sparse.cols).collect::<Vec<_>>();
820 sparse
821 .with_deleted_rows(&rows)
822 .and_then(|sparse| sparse.with_deleted_columns(&columns))
823 }
824 }
825 .map_err(|error| map_slice_shape_error("sparse deletion", error))?;
826 Ok(Value::SparseTensor(updated))
827}
828
829pub async fn assign_tensor_with_plan(
830 t: Tensor,
831 plan: &IndexPlan,
832 rhs: &Value,
833) -> Result<Value, RuntimeError> {
834 if plan.indices.is_empty() {
835 return Ok(Value::Tensor(t));
836 }
837 if matches!(rhs, Value::Complex(_, _) | Value::ComplexTensor(_)) {
838 let tensor = real_tensor_to_complex(t, "slice complex promotion")?;
839 return assign_complex_with_plan(tensor, plan, rhs).await;
840 }
841 let shape = t.shape.clone();
842 let storage = t
843 .into_numeric_storage()
844 .map_err(|error| map_slice_shape_error("slice assign", error))?;
845 let storage = match storage.into_integer_storage() {
846 Ok(mut storage) => {
847 let rhs_values = materialize_integer_rhs_for_plan(rhs, plan).await?;
848 integer_assignment::scatter(&mut storage, plan, &rhs_values)?;
849 NumericStorage::from_integer_storage(storage)
850 }
851 Err(mut storage) => {
852 let rhs_values = materialize_rhs_real_for_plan(rhs, plan).await?;
853 scatter_real_with_plan(&mut storage, plan, &rhs_values)?;
854 storage
855 }
856 };
857 Tensor::from_numeric_storage(storage, shape)
858 .map(Value::Tensor)
859 .map_err(|error| map_slice_shape_error("slice assign", error))
860}
861
862pub async fn assign_complex_with_plan(
863 mut tensor: ComplexTensor,
864 plan: &IndexPlan,
865 rhs: &Value,
866) -> Result<Value, RuntimeError> {
867 if plan.indices.is_empty() {
868 return Ok(Value::ComplexTensor(tensor));
869 }
870 if tensor.integer_storage().is_some() {
871 let rhs_values = materialize_complex_integer_rhs_for_plan(rhs, plan).await?;
872 let storage = tensor
873 .integer_storage()
874 .cloned()
875 .expect("typed complex tensor must retain exact storage");
876 let real_values: Vec<IntegerAssignmentValue> =
877 rhs_values.iter().map(|value| value.real.clone()).collect();
878 let imag_values: Vec<IntegerAssignmentValue> =
879 rhs_values.iter().map(|value| value.imag.clone()).collect();
880 let mut real = storage.real;
881 let mut imag = storage.imag;
882 integer_assignment::scatter(&mut real, plan, &real_values)?;
883 integer_assignment::scatter(&mut imag, plan, &imag_values)?;
884 return IntegerComplexStorage::new(real, imag)
885 .and_then(|storage| ComplexTensor::new_integer(storage, tensor.shape))
886 .map(Value::ComplexTensor)
887 .map_err(|error| map_slice_shape_error("typed complex slice assign", error));
888 }
889 let rhs_view = build_complex_rhs_view(rhs, &plan.selection_lengths)?;
890 scatter_complex_with_plan(&mut tensor, plan, &rhs_view)?;
891 Ok(Value::ComplexTensor(tensor))
892}
893
894pub fn delete_tensor_with_plan(
895 t: Tensor,
896 plan: &IndexPlan,
897 rhs: &Value,
898) -> Result<Value, RuntimeError> {
899 if !is_empty_delete_rhs(rhs) {
900 return Err(mex(
901 "DeletionRequiresEmptyRhs",
902 "Indexed deletion requires empty RHS",
903 ));
904 }
905 if plan.indices.is_empty() {
906 return Ok(Value::Tensor(t));
907 }
908 if !(t.rows == 1 || t.cols == 1) {
909 return Err(mex(
910 "UnsupportedDeletion",
911 "Linear deletion is only supported for vectors",
912 ));
913 }
914 let positions = sorted_unique_positions_desc(plan, tensor_element_len(&t))?;
915 let rows = t.rows;
916 let cols = t.cols;
917 let mut storage = t
918 .into_numeric_storage()
919 .map_err(|error| map_slice_shape_error("slice deletion", error))?;
920 storage
921 .remove_positions(&positions)
922 .map_err(|error| map_slice_shape_error("slice deletion", error))?;
923 let shape = deleted_vector_shape(rows, cols, storage.len());
924 Tensor::from_numeric_storage(storage, shape)
925 .map(Value::Tensor)
926 .map_err(|error| map_slice_shape_error("slice deletion", error))
927}
928
929pub fn delete_complex_with_plan(
930 t: ComplexTensor,
931 plan: &IndexPlan,
932 rhs: &Value,
933) -> Result<Value, RuntimeError> {
934 if !is_empty_delete_rhs(rhs) {
935 return Err(mex(
936 "DeletionRequiresEmptyRhs",
937 "Indexed deletion requires empty RHS",
938 ));
939 }
940 if plan.indices.is_empty() {
941 return Ok(Value::ComplexTensor(t));
942 }
943 if !(t.rows == 1 || t.cols == 1) {
944 return Err(mex(
945 "UnsupportedDeletion",
946 "Linear deletion is only supported for vectors",
947 ));
948 }
949 let positions = sorted_unique_positions_desc(plan, complex_tensor_element_len(&t))?;
950 if let Some(storage) = t.integer_storage().cloned() {
951 let storage = delete_integer_complex_storage_positions(storage, &positions);
952 let shape = deleted_vector_shape(t.rows, t.cols, storage.len());
953 return ComplexTensor::new_integer(storage, shape)
954 .map(Value::ComplexTensor)
955 .map_err(|error| map_slice_shape_error("complex slice deletion", error));
956 }
957 let dtype = t.numeric_dtype();
958 let rows = t.rows;
959 let cols = t.cols;
960 let mut values = t.materialize_f64();
961 for pos in positions {
962 values.remove(pos);
963 }
964 let shape = deleted_vector_shape(rows, cols, values.len());
965 ComplexTensor::from_f64_values_with_dtype(values, shape, dtype)
966 .map(Value::ComplexTensor)
967 .map_err(|error| map_slice_shape_error("complex slice deletion", error))
968}
969
970pub async fn assign_gpu_slice_with_plan(
971 handle: &runmat_accelerate_api::GpuTensorHandle,
972 plan: &IndexPlan,
973 rhs: &Value,
974) -> Result<Value, RuntimeError> {
975 if plan.indices.is_empty() {
976 return Ok(Value::GpuTensor(handle.clone()));
977 }
978 let mut unique_indices = plan.indices.clone();
979 unique_indices.sort_unstable();
980 if unique_indices.windows(2).any(|pair| pair[0] == pair[1]) {
981 return Err(mex(
982 "RepeatedGpuAssignmentIndex",
983 "gpuArray indexed assignment does not support repeated target subscripts",
984 ));
985 }
986 let provider = runmat_accelerate_api::provider_for_handle(handle).ok_or_else(|| {
987 mex(
988 "AccelerationProviderUnavailable",
989 "No acceleration provider owns the target gpuArray",
990 )
991 })?;
992 if runmat_accelerate_api::handle_integer_type(handle).is_some() {
993 if let Value::GpuTensor(rhs_handle) = rhs {
994 if runmat_accelerate_api::handle_integer_type(rhs_handle)
995 == runmat_accelerate_api::handle_integer_type(handle)
996 {
997 if let Ok(rhs_indices) = integer_gpu_rhs_indices_for_plan(&rhs_handle.shape, plan) {
998 let reuses_rhs = rhs_indices
999 .iter()
1000 .enumerate()
1001 .all(|(index, &rhs_index)| rhs_index as usize == index);
1002 let values = if reuses_rhs {
1003 rhs_handle.clone()
1004 } else {
1005 provider
1006 .gather_linear(rhs_handle, &rhs_indices, &plan.output_shape)
1007 .map_err(|e| {
1008 map_acceleration_error(
1009 "expand exact integer gpuArray assignment rhs",
1010 e,
1011 )
1012 })?
1013 };
1014 let result = provider
1015 .scatter_linear(handle, &plan.indices, &values)
1016 .map_err(|e| {
1017 map_acceleration_error("exact integer gpuArray slice assignment", e)
1018 });
1019 if !reuses_rhs {
1020 let _ = provider.free(&values);
1021 }
1022 result?;
1023 return Ok(Value::GpuTensor(handle.clone()));
1024 }
1025 }
1026 }
1027 let tensor = download_integer_tensor(handle).await?;
1028 let Value::Tensor(updated) = assign_tensor_with_plan(tensor, plan, rhs).await? else {
1029 unreachable!("real integer slice assignment must produce a real tensor")
1030 };
1031 return upload_tensor_to_gpu(&updated);
1032 }
1033 if let Value::GpuTensor(vh) = rhs {
1034 let rows = plan.base_shape.first().copied().unwrap_or(1);
1035 let cols = plan.base_shape.get(1).copied().unwrap_or(1);
1036 if let Some(col) = plan.properties.full_column {
1037 if col < cols {
1038 let v_rows = match vh.shape.len() {
1039 1 | 2 => vh.shape[0],
1040 _ => 0,
1041 };
1042 if v_rows == rows {
1043 if let Ok(new_h) = provider.scatter_column(handle, col, vh) {
1044 return Ok(Value::GpuTensor(new_h));
1045 }
1046 }
1047 }
1048 }
1049 if let Some(row) = plan.properties.full_row {
1050 if row < rows {
1051 let v_cols = match vh.shape.len() {
1052 1 => vh.shape[0],
1053 2 => vh.shape[1],
1054 _ => 0,
1055 };
1056 if v_cols == cols {
1057 if let Ok(new_h) = provider.scatter_row(handle, row, vh) {
1058 return Ok(Value::GpuTensor(new_h));
1059 }
1060 }
1061 }
1062 }
1063 }
1064 let rhs_values = materialize_rhs_real_for_plan(rhs, plan).await?;
1065 let value_shape = vec![rhs_values.len().max(1), 1];
1066 let upload_result = if rhs_values.is_empty() {
1067 provider.zeros(&[0, 1])
1068 } else {
1069 provider.upload(&runmat_accelerate_api::HostTensorView {
1070 data: &rhs_values,
1071 shape: &value_shape,
1072 })
1073 };
1074 if let Ok(values_handle) = upload_result {
1075 if provider
1076 .scatter_linear(handle, &plan.indices, &values_handle)
1077 .is_ok()
1078 {
1079 return Ok(Value::GpuTensor(handle.clone()));
1080 }
1081 }
1082
1083 let host = provider
1084 .download(handle)
1085 .await
1086 .map_err(|e| map_acceleration_error("gather for slice assign", e))?;
1087 let t =
1088 Tensor::new(host.data, host.shape).map_err(|e| map_slice_shape_error("slice assign", e))?;
1089 let shape = t.shape.clone();
1090 let mut storage = t
1091 .into_numeric_storage()
1092 .map_err(|error| map_slice_shape_error("slice assign", error))?;
1093 scatter_real_with_plan(&mut storage, plan, &rhs_values)?;
1094 let t = Tensor::from_numeric_storage(storage, shape)
1095 .map_err(|error| map_slice_shape_error("slice assign", error))?;
1096 upload_tensor_to_gpu(&t)
1097}
1098
1099fn integer_gpu_rhs_indices_for_plan(
1100 rhs_shape: &[usize],
1101 plan: &IndexPlan,
1102) -> Result<Vec<u32>, RuntimeError> {
1103 let rhs_len = rhs_shape.iter().try_fold(1usize, |len, dimension| {
1104 len.checked_mul(*dimension)
1105 .ok_or_else(|| mex("ShapeMismatch", "shape mismatch for slice assign"))
1106 })?;
1107 if plan.dims == 1 {
1108 if rhs_len == plan.indices.len() {
1109 return (0..rhs_len)
1110 .map(|index| {
1111 u32::try_from(index).map_err(|_| {
1112 mex(
1113 "AccelerationOperationFailed",
1114 "GPU rhs exceeds indexing limits",
1115 )
1116 })
1117 })
1118 .collect();
1119 }
1120 if rhs_len == 1 {
1121 return Ok(vec![0; plan.indices.len()]);
1122 }
1123 return Err(mex("ShapeMismatch", "shape mismatch for slice assign"));
1124 }
1125
1126 let dims = plan.selection_lengths.len();
1127 let shape = validated_assignment_rhs_shape(rhs_shape, rhs_len, &plan.selection_lengths)?;
1128 let mut strides = vec![1usize; dims];
1129 for dimension in 1..dims {
1130 strides[dimension] = strides[dimension - 1]
1131 .checked_mul(shape[dimension - 1].max(1))
1132 .ok_or_else(|| mex("ShapeMismatch", "shape mismatch for slice assign"))?;
1133 }
1134 let mut output = Vec::with_capacity(plan.indices.len());
1135 let mut coordinates = vec![0usize; dims];
1136 for _ in 0..plan.indices.len() {
1137 let mut rhs_index = 0usize;
1138 for dimension in 0..dims {
1139 let coordinate = if shape[dimension] == 1 {
1140 0
1141 } else {
1142 coordinates[dimension]
1143 };
1144 rhs_index = rhs_index
1145 .checked_add(
1146 coordinate
1147 .checked_mul(strides[dimension])
1148 .ok_or_else(|| mex("ShapeMismatch", "shape mismatch for slice assign"))?,
1149 )
1150 .ok_or_else(|| mex("ShapeMismatch", "shape mismatch for slice assign"))?;
1151 }
1152 output.push(u32::try_from(rhs_index).map_err(|_| {
1153 mex(
1154 "AccelerationOperationFailed",
1155 "GPU rhs exceeds indexing limits",
1156 )
1157 })?);
1158 for (dimension, coordinate) in coordinates.iter_mut().enumerate().take(dims) {
1159 *coordinate += 1;
1160 if *coordinate < plan.selection_lengths[dimension].max(1) {
1161 break;
1162 }
1163 *coordinate = 0;
1164 }
1165 }
1166 Ok(output)
1167}
1168
1169pub async fn delete_gpu_slice_with_plan(
1170 handle: &runmat_accelerate_api::GpuTensorHandle,
1171 plan: &IndexPlan,
1172 rhs: &Value,
1173) -> Result<Value, RuntimeError> {
1174 if !is_empty_delete_rhs(rhs) {
1175 return Err(mex(
1176 "DeletionRequiresEmptyRhs",
1177 "Indexed deletion requires empty RHS",
1178 ));
1179 }
1180 if plan.indices.is_empty() {
1181 return Ok(Value::GpuTensor(handle.clone()));
1182 }
1183 let provider = runmat_accelerate_api::provider().ok_or_else(|| {
1184 mex(
1185 "AccelerationProviderUnavailable",
1186 "No acceleration provider registered",
1187 )
1188 })?;
1189 if runmat_accelerate_api::handle_integer_type(handle).is_some() {
1190 let tensor = download_integer_tensor(handle).await?;
1191 let Value::Tensor(updated) = delete_tensor_with_plan(tensor, plan, rhs)? else {
1192 unreachable!("integer slice deletion must produce a real tensor")
1193 };
1194 return upload_tensor_to_gpu(&updated);
1195 }
1196 let host = provider
1197 .download(handle)
1198 .await
1199 .map_err(|e| map_acceleration_error("gather for slice deletion", e))?;
1200 let t = Tensor::new(host.data, host.shape)
1201 .map_err(|e| map_slice_shape_error("slice deletion", e))?;
1202 let Value::Tensor(updated) = delete_tensor_with_plan(t, plan, rhs)? else {
1203 unreachable!()
1204 };
1205 upload_tensor_to_gpu(&updated)
1206}
1207
1208pub async fn materialize_rhs_linear_real(
1209 rhs: &Value,
1210 count: usize,
1211) -> Result<Vec<f64>, RuntimeError> {
1212 let host_rhs = crate::dispatcher::gather_if_needed_async(rhs).await?;
1213 match host_rhs {
1214 Value::Num(n) => Ok(vec![n; count]),
1215 Value::Int(int_val) => Ok(vec![int_val.to_f64(); count]),
1216 Value::Bool(b) => Ok(vec![if b { 1.0 } else { 0.0 }; count]),
1217 Value::Tensor(t) => {
1218 let len = tensor_element_len(&t);
1219 if len == count {
1220 Ok(tensor::tensor_into_values_f64(t))
1221 } else if len == 1 {
1222 Ok(vec![tensor_value_f64(&t, 0); count])
1223 } else {
1224 Err(mex("ShapeMismatch", "shape mismatch for slice assign"))
1225 }
1226 }
1227 Value::LogicalArray(la) => {
1228 if la.data.len() == count {
1229 Ok(la
1230 .data
1231 .into_iter()
1232 .map(|b| if b != 0 { 1.0 } else { 0.0 })
1233 .collect())
1234 } else if la.data.len() == 1 {
1235 let val = if la.data[0] != 0 { 1.0 } else { 0.0 };
1236 Ok(vec![val; count])
1237 } else {
1238 Err(mex("ShapeMismatch", "shape mismatch for slice assign"))
1239 }
1240 }
1241 Value::OutputList(values) => materialize_output_list_real(&values, count),
1242 other => Err(mex(
1243 "InvalidSliceAssignmentRhs",
1244 format!("slice assign: unsupported RHS type {:?}", other),
1245 )),
1246 }
1247}
1248
1249pub async fn materialize_rhs_nd_real(
1250 rhs: &Value,
1251 selection_lengths: &[usize],
1252) -> Result<Vec<f64>, RuntimeError> {
1253 let rhs_host = crate::dispatcher::gather_if_needed_async(rhs).await?;
1254 enum RhsView {
1255 Scalar(f64),
1256 Tensor {
1257 data: Vec<f64>,
1258 shape: Vec<usize>,
1259 strides: Vec<usize>,
1260 },
1261 }
1262 let view = match rhs_host {
1263 Value::Num(n) => RhsView::Scalar(n),
1264 Value::Int(iv) => RhsView::Scalar(iv.to_f64()),
1265 Value::Bool(b) => RhsView::Scalar(if b { 1.0 } else { 0.0 }),
1266 Value::Tensor(t) => {
1267 let shape = validated_assignment_rhs_shape(
1268 &t.shape,
1269 tensor_element_len(&t),
1270 selection_lengths,
1271 )?;
1272 let mut strides = vec![1usize; selection_lengths.len()];
1273 for d in 1..selection_lengths.len() {
1274 strides[d] = strides[d - 1] * shape[d - 1].max(1);
1275 }
1276 let data = tensor::tensor_into_values_f64(t);
1277 if data.len()
1278 != shape
1279 .iter()
1280 .copied()
1281 .fold(1usize, |acc, len| acc.saturating_mul(len.max(1)))
1282 {
1283 return Err(mex("ShapeMismatch", "shape mismatch for slice assign"));
1284 }
1285 RhsView::Tensor {
1286 data,
1287 shape,
1288 strides,
1289 }
1290 }
1291 Value::LogicalArray(la) => {
1292 let shape =
1293 validated_assignment_rhs_shape(&la.shape, la.data.len(), selection_lengths)?;
1294 let mut strides = vec![1usize; selection_lengths.len()];
1295 for d in 1..selection_lengths.len() {
1296 strides[d] = strides[d - 1] * shape[d - 1].max(1);
1297 }
1298 if la.data.len()
1299 != shape
1300 .iter()
1301 .copied()
1302 .fold(1usize, |acc, len| acc.saturating_mul(len.max(1)))
1303 {
1304 return Err(mex("ShapeMismatch", "shape mismatch for slice assign"));
1305 }
1306 let data: Vec<f64> = la
1307 .data
1308 .into_iter()
1309 .map(|b| if b != 0 { 1.0 } else { 0.0 })
1310 .collect();
1311 RhsView::Tensor {
1312 data,
1313 shape,
1314 strides,
1315 }
1316 }
1317 Value::OutputList(values) => {
1318 let count = selection_lengths
1319 .iter()
1320 .copied()
1321 .fold(1usize, |acc, len| acc.saturating_mul(len.max(1)));
1322 let data = materialize_output_list_real(&values, count)?;
1323 let shape = if selection_lengths.is_empty() {
1324 vec![1]
1325 } else {
1326 selection_lengths.to_vec()
1327 };
1328 let mut strides = vec![1usize; shape.len()];
1329 for d in 1..shape.len() {
1330 strides[d] = strides[d - 1] * shape[d - 1].max(1);
1331 }
1332 RhsView::Tensor {
1333 data,
1334 shape,
1335 strides,
1336 }
1337 }
1338 other => {
1339 return Err(mex(
1340 "InvalidSliceAssignmentRhs",
1341 format!("slice assign: unsupported RHS type {:?}", other),
1342 ))
1343 }
1344 };
1345
1346 let total = selection_lengths
1347 .iter()
1348 .copied()
1349 .fold(1usize, |acc, len| acc.saturating_mul(len.max(1)));
1350 let mut out = Vec::with_capacity(total);
1351 let mut idx = vec![0usize; selection_lengths.len()];
1352 if selection_lengths.is_empty() {
1353 return Ok(out);
1354 }
1355 loop {
1356 match &view {
1357 RhsView::Scalar(val) => out.push(*val),
1358 RhsView::Tensor {
1359 data,
1360 shape,
1361 strides,
1362 } => {
1363 let mut rlin = 0usize;
1364 for d in 0..idx.len() {
1365 let rhs_len = shape[d];
1366 let pos = if rhs_len == 1 { 0 } else { idx[d] };
1367 rlin += pos * strides[d];
1368 }
1369 out.push(data.get(rlin).copied().unwrap_or(0.0));
1370 }
1371 }
1372 let mut d = 0usize;
1373 while d < idx.len() {
1374 idx[d] += 1;
1375 if idx[d] < selection_lengths[d].max(1) {
1376 break;
1377 }
1378 idx[d] = 0;
1379 d += 1;
1380 }
1381 if d == idx.len() {
1382 break;
1383 }
1384 }
1385 Ok(out)
1386}
1387
1388fn materialize_output_list_real(values: &[Value], count: usize) -> Result<Vec<f64>, RuntimeError> {
1389 if values.len() == count {
1390 values.iter().map(value_to_real_scalar).collect()
1391 } else if values.len() == 1 {
1392 let value = value_to_real_scalar(&values[0])?;
1393 Ok(vec![value; count])
1394 } else {
1395 Err(mex("ShapeMismatch", "shape mismatch for slice assign"))
1396 }
1397}
1398
1399fn value_to_real_scalar(value: &Value) -> Result<f64, RuntimeError> {
1400 match value {
1401 Value::Num(n) => Ok(*n),
1402 Value::Int(int_val) => Ok(int_val.to_f64()),
1403 Value::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }),
1404 Value::Tensor(t) if is_scalar_tensor(t) => Ok(tensor_value_f64(t, 0)),
1405 _ => f64::try_from(value).map_err(Into::into),
1406 }
1407}
1408
1409pub fn upload_tensor_to_gpu(t: &Tensor) -> Result<Value, RuntimeError> {
1410 let provider = runmat_accelerate_api::provider().ok_or_else(|| {
1411 mex(
1412 "AccelerationProviderUnavailable",
1413 "No acceleration provider registered",
1414 )
1415 })?;
1416 let new_h = if let Some(storage) = t.integer_storage() {
1417 let view = integer_tensor_view(storage, &t.shape);
1418 provider
1419 .upload_integer(&view)
1420 .map_err(|e| map_acceleration_error("exact integer reupload after slice assign", e))?
1421 } else {
1422 let data = t.materialize_f64();
1423 let view = runmat_accelerate_api::HostTensorView {
1424 data: &data,
1425 shape: &t.shape,
1426 };
1427 provider
1428 .upload(&view)
1429 .map_err(|e| map_acceleration_error("reupload after slice assign", e))?
1430 };
1431 Ok(Value::GpuTensor(new_h))
1432}
1433
1434pub(crate) async fn download_integer_tensor(
1435 handle: &runmat_accelerate_api::GpuTensorHandle,
1436) -> Result<Tensor, RuntimeError> {
1437 let provider = runmat_accelerate_api::provider_for_handle(handle).ok_or_else(|| {
1438 mex(
1439 "AccelerationProviderUnavailable",
1440 "No acceleration provider registered for integer gpuArray",
1441 )
1442 })?;
1443 let integer = provider
1444 .download_integer(handle)
1445 .await
1446 .map_err(|e| map_acceleration_error("exact integer gather for assignment", e))?;
1447 let storage = match integer.data {
1448 HostIntegerDataOwned::I8(values) => IntegerStorage::I8(values),
1449 HostIntegerDataOwned::I16(values) => IntegerStorage::I16(values),
1450 HostIntegerDataOwned::I32(values) => IntegerStorage::I32(values),
1451 HostIntegerDataOwned::I64(values) => IntegerStorage::I64(values),
1452 HostIntegerDataOwned::U8(values) => IntegerStorage::U8(values),
1453 HostIntegerDataOwned::U16(values) => IntegerStorage::U16(values),
1454 HostIntegerDataOwned::U32(values) => IntegerStorage::U32(values),
1455 HostIntegerDataOwned::U64(values) => IntegerStorage::U64(values),
1456 };
1457 Tensor::new_integer(storage, integer.shape)
1458 .map_err(|e| map_slice_shape_error("integer gpuArray assignment gather", e))
1459}
1460
1461fn integer_tensor_view<'a>(
1462 storage: &'a IntegerStorage,
1463 shape: &'a [usize],
1464) -> HostIntegerTensorView<'a> {
1465 let data = match storage {
1466 IntegerStorage::I8(values) => HostIntegerDataView::I8(values),
1467 IntegerStorage::I16(values) => HostIntegerDataView::I16(values),
1468 IntegerStorage::I32(values) => HostIntegerDataView::I32(values),
1469 IntegerStorage::I64(values) => HostIntegerDataView::I64(values),
1470 IntegerStorage::U8(values) => HostIntegerDataView::U8(values),
1471 IntegerStorage::U16(values) => HostIntegerDataView::U16(values),
1472 IntegerStorage::U32(values) => HostIntegerDataView::U32(values),
1473 IntegerStorage::U64(values) => HostIntegerDataView::U64(values),
1474 };
1475 HostIntegerTensorView { data, shape }
1476}
1477
1478#[cfg(test)]
1479mod tests {
1480 use super::{
1481 assign_complex_with_plan, assign_gpu_slice_with_plan, assign_sparse_with_plan,
1482 assign_tensor_with_plan, build_complex_rhs_view, build_string_rhs_view,
1483 delete_complex_with_plan, delete_gpu_slice_with_plan, delete_tensor_with_plan,
1484 integer_gpu_rhs_indices_for_plan, map_acceleration_error, materialize_rhs_linear_real,
1485 materialize_rhs_nd_real, validated_assignment_rhs_shape, ComplexRhsView, StringRhsView,
1486 };
1487 use crate::indexing::plan::IndexPlan;
1488 use futures::executor::block_on;
1489 use runmat_value::{
1490 CellArray, ComplexTensor, IntegerComplexStorage, IntegerStorage, NumericDType,
1491 NumericStorage, SparseTensor, StringArray, Tensor, Value,
1492 };
1493
1494 #[test]
1495 fn gpu_assignment_rejects_repeated_target_subscripts_before_provider_access() {
1496 let handle = runmat_accelerate_api::GpuTensorHandle {
1497 shape: vec![1, 3],
1498 device_id: 91,
1499 buffer_id: 17,
1500 descriptor: Default::default(),
1501 };
1502 let plan = IndexPlan::new(vec![1, 1], vec![1, 2], vec![2], 1, vec![1, 3]);
1503 let error = block_on(assign_gpu_slice_with_plan(&handle, &plan, &Value::Num(4.0)))
1504 .expect_err("repeated gpuArray assignment targets must reject");
1505 assert_eq!(
1506 error.identifier(),
1507 Some("RunMat:RepeatedGpuAssignmentIndex")
1508 );
1509 }
1510
1511 #[test]
1512 fn integer_plan_assignment_preserves_exact_uint64_rhs() {
1513 let tensor =
1514 Tensor::new_integer(IntegerStorage::U64(vec![1, 2, 3]), vec![1, 3]).expect("tensor");
1515 let rhs_tensor =
1516 Tensor::new_integer(IntegerStorage::U64(vec![u64::MAX, 9]), vec![1, 2]).expect("rhs");
1517 let rhs = Value::Tensor(rhs_tensor);
1518 let plan = IndexPlan::new(vec![0, 1], vec![1, 2], vec![2], 1, vec![1, 3]);
1519 let result = block_on(assign_tensor_with_plan(tensor, &plan, &rhs)).expect("assign");
1520
1521 let Value::Tensor(output) = result else {
1522 panic!("expected tensor");
1523 };
1524 assert_eq!(
1525 output.integer_storage(),
1526 Some(&IntegerStorage::U64(vec![u64::MAX, 9, 3]))
1527 );
1528 }
1529
1530 #[test]
1531 fn native_single_plan_assignment_and_deletion_preserve_class() {
1532 let tensor = Tensor::from_f32(vec![1.0, 2.0, 3.0], vec![1, 3]).expect("single tensor");
1533 let plan = IndexPlan::new(vec![0, 2], vec![1, 2], vec![2], 1, vec![1, 3]);
1534 let Value::Tensor(updated) = block_on(assign_tensor_with_plan(
1535 tensor,
1536 &plan,
1537 &Value::Num(1.234_567_890_123),
1538 ))
1539 .expect("single slice assignment") else {
1540 panic!("expected tensor");
1541 };
1542 assert_eq!(updated.numeric_dtype(), NumericDType::F32);
1543 assert_eq!(
1544 updated.clone().into_numeric_storage(),
1545 Ok(NumericStorage::F32(vec![
1546 1.234_567_890_123_f64 as f32,
1547 2.0,
1548 1.234_567_890_123_f64 as f32,
1549 ]))
1550 );
1551
1552 let empty = Value::Tensor(Tensor::new(Vec::new(), vec![0, 0]).expect("empty"));
1553 let delete_plan = IndexPlan::new(vec![1], vec![1, 1], vec![1], 1, vec![1, 3]);
1554 let Value::Tensor(deleted) =
1555 delete_tensor_with_plan(updated, &delete_plan, &empty).expect("single slice deletion")
1556 else {
1557 panic!("expected tensor");
1558 };
1559 assert_eq!(deleted.numeric_dtype(), NumericDType::F32);
1560 assert_eq!(deleted.shape, vec![1, 2]);
1561 assert_eq!(
1562 deleted.into_numeric_storage(),
1563 Ok(NumericStorage::F32(vec![
1564 1.234_567_890_123_f64 as f32,
1565 1.234_567_890_123_f64 as f32,
1566 ]))
1567 );
1568 }
1569
1570 #[test]
1571 fn native_complex_single_plan_assignment_and_deletion_preserve_class() {
1572 let tensor =
1573 ComplexTensor::from_f32(vec![(1.0, -1.0), (2.0, -2.0), (3.0, -3.0)], vec![1, 3])
1574 .unwrap();
1575 let plan = IndexPlan::new(vec![0, 2], vec![1, 2], vec![2], 1, vec![1, 3]);
1576 let Value::ComplexTensor(updated) = block_on(assign_complex_with_plan(
1577 tensor,
1578 &plan,
1579 &Value::Complex(1.234_567_890_123, -9.876_543_210_987),
1580 ))
1581 .expect("complex single slice assignment") else {
1582 panic!("expected complex tensor");
1583 };
1584 assert_eq!(updated.numeric_dtype(), NumericDType::F32);
1585 assert_eq!(
1586 updated.as_f32_slice(),
1587 Some(
1588 &[
1589 (1.234_567_890_123_f64 as f32, -9.876_543_210_987_f64 as f32,),
1590 (2.0, -2.0),
1591 (1.234_567_890_123_f64 as f32, -9.876_543_210_987_f64 as f32,),
1592 ][..]
1593 )
1594 );
1595
1596 let empty = Value::Tensor(Tensor::new(Vec::new(), vec![0, 0]).expect("empty"));
1597 let delete_plan = IndexPlan::new(vec![1], vec![1, 1], vec![1], 1, vec![1, 3]);
1598 let Value::ComplexTensor(deleted) = delete_complex_with_plan(updated, &delete_plan, &empty)
1599 .expect("complex single slice deletion")
1600 else {
1601 panic!("expected complex tensor");
1602 };
1603 assert_eq!(deleted.numeric_dtype(), NumericDType::F32);
1604 assert_eq!(deleted.shape, vec![1, 2]);
1605 assert_eq!(
1606 deleted.as_f32_slice(),
1607 Some(
1608 &[
1609 (1.234_567_890_123_f64 as f32, -9.876_543_210_987_f64 as f32,),
1610 (1.234_567_890_123_f64 as f32, -9.876_543_210_987_f64 as f32,),
1611 ][..]
1612 )
1613 );
1614 }
1615
1616 #[test]
1617 fn typed_slice_deletion_uses_exact_storage_when_f64_mirrors_are_unavailable() {
1618 let tensor = Tensor::new_integer(IntegerStorage::U64(vec![1, u64::MAX, 3]), vec![1, 3])
1619 .expect("tensor");
1620 let plan = IndexPlan::new(vec![1], vec![1, 1], vec![1], 1, vec![1, 3]);
1621 let empty = Value::Tensor(Tensor::new(Vec::new(), vec![0, 0]).expect("empty rhs"));
1622
1623 let Value::Tensor(output) =
1624 delete_tensor_with_plan(tensor, &plan, &empty).expect("typed uint64 deletion")
1625 else {
1626 panic!("expected tensor");
1627 };
1628 assert_eq!(output.shape, vec![1, 2]);
1629 assert_eq!(
1630 output.integer_storage(),
1631 Some(&IntegerStorage::U64(vec![1, 3]))
1632 );
1633
1634 let complex = ComplexTensor::new_integer(
1635 IntegerComplexStorage::new(
1636 IntegerStorage::I64(vec![i64::MIN, -2, i64::MAX]),
1637 IntegerStorage::I64(vec![1, 2, 3]),
1638 )
1639 .expect("integer complex storage"),
1640 vec![1, 3],
1641 )
1642 .expect("complex tensor");
1643
1644 let Value::ComplexTensor(output) = delete_complex_with_plan(complex, &plan, &empty)
1645 .expect("typed signed complex deletion")
1646 else {
1647 panic!("expected complex tensor");
1648 };
1649 assert_eq!(output.shape, vec![1, 2]);
1650 assert_eq!(
1651 output.integer_storage().cloned(),
1652 Some(
1653 IntegerComplexStorage::new(
1654 IntegerStorage::I64(vec![i64::MIN, i64::MAX]),
1655 IntegerStorage::I64(vec![1, 3]),
1656 )
1657 .expect("integer complex storage")
1658 )
1659 );
1660 }
1661
1662 #[test]
1663 fn integer_plan_assignment_preserves_all_typed_rhs_classes_with_poisoned_mirrors() {
1664 macro_rules! assert_assignment {
1665 ($storage:ident, $value:expr) => {{
1666 let tensor = Tensor::new_integer(
1667 IntegerStorage::$storage(vec![Default::default(), Default::default()]),
1668 vec![1, 2],
1669 )
1670 .expect("destination tensor");
1671 let rhs = Tensor::new_integer(IntegerStorage::$storage(vec![$value]), vec![1, 1])
1672 .expect("rhs tensor");
1673 let plan = IndexPlan::new(vec![1], vec![1, 1], vec![1], 1, vec![1, 2]);
1674
1675 let Value::Tensor(output) =
1676 block_on(assign_tensor_with_plan(tensor, &plan, &Value::Tensor(rhs)))
1677 .expect("assignment")
1678 else {
1679 panic!("expected tensor");
1680 };
1681 assert_eq!(
1682 output.integer_storage(),
1683 Some(&IntegerStorage::$storage(vec![Default::default(), $value]))
1684 );
1685 }};
1686 }
1687
1688 assert_assignment!(I8, i8::MIN);
1689 assert_assignment!(I16, i16::MIN);
1690 assert_assignment!(I32, i32::MIN);
1691 assert_assignment!(I64, i64::MIN);
1692 assert_assignment!(U8, u8::MAX);
1693 assert_assignment!(U16, u16::MAX);
1694 assert_assignment!(U32, u32::MAX);
1695 assert_assignment!(U64, u64::MAX);
1696 }
1697
1698 #[test]
1699 fn real_plan_assignment_reads_typed_integer_rhs_without_mirror() {
1700 let tensor = Tensor::new(vec![0.0, 0.0, 0.0], vec![1, 3]).expect("tensor");
1701 let rhs_tensor =
1702 Tensor::new_integer(IntegerStorage::U16(vec![4, 9]), vec![1, 2]).expect("rhs");
1703 let rhs = Value::Tensor(rhs_tensor);
1704 let plan = IndexPlan::new(vec![0, 2], vec![1, 2], vec![2], 1, vec![1, 3]);
1705 let result = block_on(assign_tensor_with_plan(tensor, &plan, &rhs)).expect("assign");
1706
1707 let Value::Tensor(output) = result else {
1708 panic!("expected tensor");
1709 };
1710 assert_eq!(output.materialize_f64(), vec![4.0, 0.0, 9.0]);
1711 }
1712
1713 #[test]
1714 fn real_scalar_expansion_reads_all_typed_integer_classes_without_f64_mirrors() {
1715 macro_rules! assert_scalar_expansion {
1716 ($storage:expr, $expected:expr) => {{
1717 let tensor = Tensor::new_integer($storage, vec![1, 1]).expect("scalar rhs");
1718 let rhs = Value::Tensor(tensor);
1719
1720 assert_eq!(
1721 block_on(materialize_rhs_linear_real(&rhs, 3)).expect("linear expansion"),
1722 vec![$expected; 3]
1723 );
1724 assert_eq!(
1725 block_on(materialize_rhs_nd_real(&rhs, &[2, 2])).expect("nd expansion"),
1726 vec![$expected; 4]
1727 );
1728 }};
1729 }
1730
1731 assert_scalar_expansion!(IntegerStorage::I8(vec![-8]), -8.0);
1732 assert_scalar_expansion!(IntegerStorage::I16(vec![-16]), -16.0);
1733 assert_scalar_expansion!(IntegerStorage::I32(vec![-32]), -32.0);
1734 assert_scalar_expansion!(IntegerStorage::I64(vec![-64]), -64.0);
1735 assert_scalar_expansion!(IntegerStorage::U8(vec![8]), 8.0);
1736 assert_scalar_expansion!(IntegerStorage::U16(vec![16]), 16.0);
1737 assert_scalar_expansion!(IntegerStorage::U32(vec![32]), 32.0);
1738 assert_scalar_expansion!(IntegerStorage::U64(vec![64]), 64.0);
1739 }
1740
1741 #[test]
1742 fn integer_plan_assignment_rejects_nonscalar_singleton_expansion() {
1743 let tensor =
1744 Tensor::new_integer(IntegerStorage::I8(vec![0; 4]), vec![2, 2]).expect("tensor");
1745 let rhs = Value::Tensor(
1746 Tensor::new_integer(IntegerStorage::I8(vec![5, 6]), vec![1, 2]).expect("rhs"),
1747 );
1748 let plan = IndexPlan::new(vec![0, 1, 2, 3], vec![2, 2], vec![2, 2], 2, vec![2, 2]);
1749 let error = block_on(assign_tensor_with_plan(tensor, &plan, &rhs))
1750 .expect_err("nonscalar singleton expansion must reject");
1751 assert_eq!(error.identifier(), Some("RunMat:ShapeMismatch"));
1752 }
1753
1754 #[test]
1755 fn assignment_rhs_shape_allows_only_scalar_linear_count_or_exact_nd_shape() {
1756 assert_eq!(
1757 validated_assignment_rhs_shape(&[1, 1, 1], 1, &[2, 3]).expect("scalar"),
1758 vec![1, 1]
1759 );
1760 assert_eq!(
1761 validated_assignment_rhs_shape(&[1, 6], 6, &[6]).expect("linear count"),
1762 vec![6]
1763 );
1764 assert_eq!(
1765 validated_assignment_rhs_shape(&[2, 3, 1], 6, &[2, 3]).expect("exact shape"),
1766 vec![2, 3]
1767 );
1768 assert!(validated_assignment_rhs_shape(&[1, 3], 3, &[2, 3]).is_err());
1769 assert!(validated_assignment_rhs_shape(&[2, 1], 2, &[2, 3]).is_err());
1770 }
1771
1772 #[test]
1773 fn floating_logical_complex_and_string_rhs_reject_singleton_expansion() {
1774 let floating = Value::Tensor(Tensor::new(vec![1.0, 2.0], vec![1, 2]).expect("floating"));
1775 let error = block_on(materialize_rhs_nd_real(&floating, &[2, 2]))
1776 .expect_err("floating singleton expansion must reject");
1777 assert_eq!(error.identifier(), Some("RunMat:ShapeMismatch"));
1778
1779 let logical = Value::LogicalArray(
1780 runmat_value::LogicalArray::new(vec![1, 0], vec![1, 2]).expect("logical"),
1781 );
1782 let error = block_on(materialize_rhs_nd_real(&logical, &[2, 2]))
1783 .expect_err("logical singleton expansion must reject");
1784 assert_eq!(error.identifier(), Some("RunMat:ShapeMismatch"));
1785
1786 let complex = Value::ComplexTensor(
1787 ComplexTensor::new(vec![(1.0, 2.0), (3.0, 4.0)], vec![1, 2]).expect("complex"),
1788 );
1789 let error = match build_complex_rhs_view(&complex, &[2, 2]) {
1790 Ok(_) => panic!("complex singleton expansion must reject"),
1791 Err(error) => error,
1792 };
1793 assert_eq!(error.identifier(), Some("RunMat:ShapeMismatch"));
1794
1795 let strings = Value::StringArray(
1796 runmat_value::StringArray::new(vec!["a".to_string(), "b".to_string()], vec![1, 2])
1797 .expect("strings"),
1798 );
1799 let error = match build_string_rhs_view(&strings, &[2, 2]) {
1800 Ok(_) => panic!("string singleton expansion must reject"),
1801 Err(error) => error,
1802 };
1803 assert_eq!(error.identifier(), Some("RunMat:ShapeMismatch"));
1804 }
1805
1806 #[test]
1807 fn integer_gpu_rhs_indices_cover_linear_scalar_and_exact_nd_forms() {
1808 let linear = IndexPlan::new(vec![5, 1, 3], vec![1, 3], vec![3], 1, vec![2, 3]);
1809 assert_eq!(
1810 integer_gpu_rhs_indices_for_plan(&[1, 3], &linear).expect("linear indices"),
1811 vec![0, 1, 2]
1812 );
1813 assert_eq!(
1814 integer_gpu_rhs_indices_for_plan(&[1, 1], &linear).expect("scalar indices"),
1815 vec![0, 0, 0]
1816 );
1817
1818 let nd = IndexPlan::new(
1819 vec![0, 1, 2, 3, 4, 5],
1820 vec![2, 3],
1821 vec![2, 3],
1822 2,
1823 vec![2, 3],
1824 );
1825 assert_eq!(
1826 integer_gpu_rhs_indices_for_plan(&[2, 3], &nd).expect("exact nd indices"),
1827 vec![0, 1, 2, 3, 4, 5]
1828 );
1829 assert!(integer_gpu_rhs_indices_for_plan(&[1, 3], &nd).is_err());
1830 }
1831
1832 #[test]
1833 fn integer_gpu_rhs_indices_reject_incompatible_shape() {
1834 let plan = IndexPlan::new(vec![0, 1, 2, 3], vec![2, 2], vec![2, 2], 2, vec![2, 2]);
1835 let error = integer_gpu_rhs_indices_for_plan(&[3, 1], &plan)
1836 .expect_err("incompatible GPU rhs shape must fail");
1837 assert_eq!(error.identifier(), Some("RunMat:ShapeMismatch"));
1838 }
1839
1840 #[test]
1841 fn integer_plan_assignment_converts_float_rhs_with_saturation() {
1842 let tensor =
1843 Tensor::new_integer(IntegerStorage::I8(vec![0, 0]), vec![1, 2]).expect("tensor");
1844 let plan = IndexPlan::new(vec![0, 1], vec![1, 2], vec![2], 1, vec![1, 2]);
1845 let result =
1846 block_on(assign_tensor_with_plan(tensor, &plan, &Value::Num(300.5))).expect("assign");
1847
1848 let Value::Tensor(output) = result else {
1849 panic!("expected tensor");
1850 };
1851 assert_eq!(
1852 output.integer_storage(),
1853 Some(&IntegerStorage::I8(vec![i8::MAX, i8::MAX]))
1854 );
1855 }
1856
1857 #[test]
1858 fn real_integer_plan_assignment_promotes_to_complex_exact_storage_for_every_class() {
1859 macro_rules! assert_promotion {
1860 ($storage:ident, $values:expr, $real:expr, $imag:expr) => {{
1861 let tensor = Tensor::new_integer(IntegerStorage::$storage($values), vec![2, 2])
1862 .expect("tensor");
1863 let rhs_tensor = ComplexTensor::new_integer(
1864 IntegerComplexStorage::new(
1865 IntegerStorage::$storage(vec![$real]),
1866 IntegerStorage::$storage(vec![$imag]),
1867 )
1868 .expect("rhs storage"),
1869 vec![1, 1],
1870 )
1871 .expect("rhs");
1872 let plan = IndexPlan::new(vec![0, 1, 2, 3], vec![2, 2], vec![2, 2], 2, vec![2, 2]);
1873 let result = block_on(assign_tensor_with_plan(
1874 tensor,
1875 &plan,
1876 &Value::ComplexTensor(rhs_tensor),
1877 ))
1878 .expect("assign");
1879 let Value::ComplexTensor(output) = result else {
1880 panic!("integer tensor should promote to complex tensor");
1881 };
1882 assert_eq!(
1883 output
1884 .integer_storage()
1885 .map(|storage| (&storage.real, &storage.imag)),
1886 Some((
1887 &IntegerStorage::$storage(vec![$real; 4]),
1888 &IntegerStorage::$storage(vec![$imag; 4]),
1889 ))
1890 );
1891 }};
1892 }
1893
1894 assert_promotion!(I8, vec![i8::MIN, -1, 0, i8::MAX], i8::MIN, i8::MAX);
1895 assert_promotion!(I16, vec![i16::MIN, -1, 0, i16::MAX], i16::MIN, i16::MAX);
1896 assert_promotion!(I32, vec![i32::MIN, -1, 0, i32::MAX], i32::MIN, i32::MAX);
1897 assert_promotion!(I64, vec![i64::MIN, -1, 0, i64::MAX], i64::MIN, i64::MAX);
1898 assert_promotion!(U8, vec![0, 1, 2, u8::MAX], 1, u8::MAX);
1899 assert_promotion!(U16, vec![0, 1, 2, u16::MAX], 1, u16::MAX);
1900 assert_promotion!(U32, vec![0, 1, 2, u32::MAX], 1, u32::MAX);
1901 assert_promotion!(
1902 U64,
1903 vec![0, 1, 2, u64::MAX],
1904 9_223_372_036_854_775_809,
1905 u64::MAX
1906 );
1907 }
1908
1909 #[test]
1910 fn real_integer_complex_promotion_rejects_shape_mismatch() {
1911 let tensor =
1912 Tensor::new_integer(IntegerStorage::I16(vec![0; 4]), vec![2, 2]).expect("tensor");
1913 let rhs = Value::ComplexTensor(
1914 ComplexTensor::new_integer(
1915 IntegerComplexStorage::new(
1916 IntegerStorage::I16(vec![1, 2, 3]),
1917 IntegerStorage::I16(vec![-1, -2, -3]),
1918 )
1919 .expect("rhs storage"),
1920 vec![1, 3],
1921 )
1922 .expect("rhs"),
1923 );
1924 let plan = IndexPlan::new(vec![0, 1, 2, 3], vec![2, 2], vec![2, 2], 2, vec![2, 2]);
1925
1926 let error = block_on(assign_tensor_with_plan(tensor, &plan, &rhs))
1927 .expect_err("incompatible complex RHS shape must fail");
1928 assert_eq!(error.identifier(), Some("RunMat:ShapeMismatch"));
1929 }
1930
1931 #[test]
1932 fn real_integer_complex_promotion_preserves_exact_non_2d_scalar_expansion() {
1933 let tensor = Tensor::new_integer(IntegerStorage::U64(vec![1, 2, 3, 4]), vec![2, 1, 2])
1934 .expect("tensor");
1935 let rhs = Value::ComplexTensor(
1936 ComplexTensor::new_integer(
1937 IntegerComplexStorage::new(
1938 IntegerStorage::U64(vec![u64::MAX]),
1939 IntegerStorage::U64(vec![9_223_372_036_854_775_809]),
1940 )
1941 .expect("rhs storage"),
1942 vec![1, 1, 1],
1943 )
1944 .expect("rhs"),
1945 );
1946 let plan = IndexPlan::new(
1947 vec![0, 1, 2, 3],
1948 vec![2, 1, 2],
1949 vec![2, 1, 2],
1950 3,
1951 vec![2, 1, 2],
1952 );
1953
1954 let result = block_on(assign_tensor_with_plan(tensor, &plan, &rhs)).expect("assign");
1955 let Value::ComplexTensor(output) = result else {
1956 panic!("integer tensor should promote to complex tensor");
1957 };
1958 assert_eq!(output.shape, vec![2, 1, 2]);
1959 assert_eq!(
1960 output
1961 .integer_storage()
1962 .as_ref()
1963 .map(|storage| (&storage.real, &storage.imag)),
1964 Some((
1965 &IntegerStorage::U64(vec![u64::MAX; 4]),
1966 &IntegerStorage::U64(vec![9_223_372_036_854_775_809; 4]),
1967 ))
1968 );
1969 }
1970
1971 #[test]
1972 fn typed_complex_integer_plan_assignment_rejects_nonscalar_singleton_expansion() {
1973 let tensor = ComplexTensor::new_integer(
1974 IntegerComplexStorage::new(
1975 IntegerStorage::U64(vec![1, 2, 3, 4]),
1976 IntegerStorage::U64(vec![10, 20, 30, 40]),
1977 )
1978 .expect("storage"),
1979 vec![2, 2],
1980 )
1981 .expect("tensor");
1982 let rhs = Value::ComplexTensor(
1983 ComplexTensor::new_integer(
1984 IntegerComplexStorage::new(
1985 IntegerStorage::U64(vec![u64::MAX, 9_223_372_036_854_775_808]),
1986 IntegerStorage::U64(vec![7, 8]),
1987 )
1988 .expect("storage"),
1989 vec![1, 2],
1990 )
1991 .expect("rhs"),
1992 );
1993 let plan = IndexPlan::new(vec![0, 1, 2, 3], vec![2, 2], vec![2, 2], 2, vec![2, 2]);
1994 let error = block_on(assign_complex_with_plan(tensor, &plan, &rhs))
1995 .expect_err("complex nonscalar singleton expansion must reject");
1996 assert_eq!(error.identifier(), Some("RunMat:ShapeMismatch"));
1997 }
1998
1999 #[test]
2000 fn typed_complex_integer_plan_assignment_rejects_shape_mismatch() {
2001 let tensor = ComplexTensor::new_integer(
2002 IntegerComplexStorage::new(
2003 IntegerStorage::I8(vec![0; 4]),
2004 IntegerStorage::I8(vec![0; 4]),
2005 )
2006 .expect("storage"),
2007 vec![2, 2],
2008 )
2009 .expect("tensor");
2010 let rhs = Value::ComplexTensor(
2011 ComplexTensor::new_integer(
2012 IntegerComplexStorage::new(
2013 IntegerStorage::I8(vec![1, 2, 3]),
2014 IntegerStorage::I8(vec![4, 5, 6]),
2015 )
2016 .expect("storage"),
2017 vec![1, 3],
2018 )
2019 .expect("rhs"),
2020 );
2021 let plan = IndexPlan::new(vec![0, 1, 2, 3], vec![2, 2], vec![2, 2], 2, vec![2, 2]);
2022 let err = block_on(assign_complex_with_plan(tensor, &plan, &rhs))
2023 .expect_err("shape mismatch should fail");
2024 assert_eq!(err.identifier(), Some("RunMat:ShapeMismatch"));
2025 }
2026
2027 #[test]
2028 fn typed_complex_integer_plan_assignment_converts_float_components_independently() {
2029 let tensor = ComplexTensor::new_integer(
2030 IntegerComplexStorage::new(
2031 IntegerStorage::I8(vec![0; 2]),
2032 IntegerStorage::I8(vec![0; 2]),
2033 )
2034 .expect("storage"),
2035 vec![1, 2],
2036 )
2037 .expect("tensor");
2038 let plan = IndexPlan::new(vec![0, 1], vec![1, 2], vec![2], 1, vec![1, 2]);
2039 let result = block_on(assign_complex_with_plan(
2040 tensor,
2041 &plan,
2042 &Value::Complex(300.5, -300.5),
2043 ))
2044 .expect("assign");
2045
2046 let Value::ComplexTensor(output) = result else {
2047 panic!("expected complex tensor");
2048 };
2049 assert_eq!(
2050 output
2051 .integer_storage()
2052 .as_ref()
2053 .map(|storage| (&storage.real, &storage.imag)),
2054 Some((
2055 &IntegerStorage::I8(vec![i8::MAX, i8::MAX]),
2056 &IntegerStorage::I8(vec![i8::MIN, i8::MIN]),
2057 ))
2058 );
2059 }
2060
2061 #[test]
2062 fn sparse_integer_plan_assignment_preserves_exact_values_and_last_write_wins() {
2063 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
2064 let sparse =
2065 SparseTensor::new_integer(2, 2, vec![0, 0, 0], vec![], IntegerStorage::U64(vec![]))
2066 .expect("sparse");
2067 let rhs = Value::Tensor(
2068 Tensor::new_integer(
2069 IntegerStorage::U64(vec![1, 9_223_372_036_854_775_808, 3, u64::MAX]),
2070 vec![1, 4],
2071 )
2072 .expect("rhs"),
2073 );
2074 let plan = IndexPlan::new(vec![0, 3, 1, 3], vec![1, 4], vec![4], 1, vec![2, 2]);
2075 let result = block_on(assign_sparse_with_plan(sparse, &plan, &rhs)).expect("assign");
2076
2077 let Value::SparseTensor(output) = result else {
2078 panic!("expected sparse output");
2079 };
2080 assert_eq!(output.col_ptrs, vec![0, 2, 3]);
2081 assert_eq!(output.row_indices, vec![0, 1, 1]);
2082 assert_eq!(
2083 output.integer_storage(),
2084 Some(&IntegerStorage::U64(vec![1, 3, u64::MAX]))
2085 );
2086 }
2087
2088 #[test]
2089 fn sparse_integer_plan_assignment_rejects_nonscalar_singleton_expansion() {
2090 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
2091 let sparse =
2092 SparseTensor::new_integer(2, 2, vec![0, 0, 0], vec![], IntegerStorage::I8(vec![]))
2093 .expect("sparse");
2094 let rhs = Value::Tensor(
2095 Tensor::new_integer(IntegerStorage::I8(vec![5, 6]), vec![1, 2]).expect("rhs"),
2096 );
2097 let plan = IndexPlan::new(vec![0, 1, 2, 3], vec![2, 2], vec![2, 2], 2, vec![2, 2]);
2098 let error = block_on(assign_sparse_with_plan(sparse, &plan, &rhs))
2099 .expect_err("sparse nonscalar singleton expansion must reject");
2100 assert_eq!(error.identifier(), Some("RunMat:ShapeMismatch"));
2101 }
2102
2103 #[test]
2104 fn sparse_real_plan_assignment_elides_zero_values() {
2105 let sparse = SparseTensor::new(2, 2, vec![0, 2, 3], vec![0, 1, 0], vec![1.0, 2.0, 3.0])
2106 .expect("sparse");
2107 let plan = IndexPlan::new(vec![0, 1], vec![2, 1], vec![2, 1], 2, vec![2, 2]);
2108 let result =
2109 block_on(assign_sparse_with_plan(sparse, &plan, &Value::Num(0.0))).expect("assign");
2110
2111 let Value::SparseTensor(output) = result else {
2112 panic!("expected sparse output");
2113 };
2114 assert_eq!(output.col_ptrs, vec![0, 0, 1]);
2115 assert_eq!(output.row_indices, vec![0]);
2116 assert_eq!(output.as_f64_slice(), Some(&[3.0][..]));
2117 }
2118
2119 #[test]
2120 fn sparse_single_plan_assignment_preserves_class_and_rounds_to_f32() {
2121 let sparse = SparseTensor::new_f32(2, 2, vec![0, 1, 2], vec![0, 1], vec![1.0, 3.0])
2122 .expect("single sparse");
2123 let plan = IndexPlan::new(vec![0, 1], vec![2, 1], vec![2, 1], 2, vec![2, 2]);
2124 let rhs = Value::Tensor(Tensor::new(vec![0.0, 1.0 / 3.0], vec![2, 1]).expect("double rhs"));
2125 let result = block_on(assign_sparse_with_plan(sparse, &plan, &rhs)).expect("assign");
2126
2127 let Value::SparseTensor(output) = result else {
2128 panic!("expected sparse output");
2129 };
2130 assert_eq!(output.numeric_dtype(), Some(NumericDType::F32));
2131 assert_eq!(output.col_ptrs, vec![0, 1, 2]);
2132 assert_eq!(output.row_indices, vec![1, 1]);
2133 assert_eq!(output.as_f32_slice(), Some(&[(1.0 / 3.0) as f32, 3.0][..]));
2134 }
2135
2136 #[test]
2137 fn sparse_logical_plan_assignment_preserves_class_and_truth_conversion() {
2138 let sparse =
2139 SparseTensor::new_logical(2, 2, vec![0, 1, 2], vec![0, 1]).expect("logical sparse");
2140 let plan = IndexPlan::new(vec![0, 1], vec![2, 1], vec![2, 1], 2, vec![2, 2]);
2141 let rhs = Value::Tensor(Tensor::new(vec![0.0, -2.0], vec![2, 1]).expect("rhs"));
2142 let result = block_on(assign_sparse_with_plan(sparse, &plan, &rhs)).expect("assign");
2143 let Value::SparseTensor(output) = result else {
2144 panic!("expected sparse output");
2145 };
2146 assert!(output.is_logical());
2147 assert_eq!(output.col_ptrs, vec![0, 1, 2]);
2148 assert_eq!(output.row_indices, vec![1, 1]);
2149 }
2150
2151 #[test]
2152 fn integer_plan_deletion_preserves_exact_storage() {
2153 let tensor = Tensor::new_integer(IntegerStorage::I64(vec![1, i64::MAX, 3]), vec![1, 3])
2154 .expect("tensor");
2155 let empty = Value::Tensor(Tensor::new(Vec::new(), vec![0, 0]).expect("empty"));
2156 let plan = IndexPlan::new(vec![1], vec![1, 1], vec![1], 1, vec![1, 3]);
2157 let result = delete_tensor_with_plan(tensor, &plan, &empty).expect("delete");
2158
2159 let Value::Tensor(output) = result else {
2160 panic!("expected tensor");
2161 };
2162 assert_eq!(
2163 output.integer_storage(),
2164 Some(&IntegerStorage::I64(vec![1, 3]))
2165 );
2166 assert_eq!(output.shape, vec![1, 2]);
2167 }
2168
2169 #[test]
2170 fn gpu_integer_plan_deletion_preserves_wide_uint64_storage() {
2171 runmat_accelerate_api::set_thread_provider(None);
2172 runmat_accelerate_api::clear_provider();
2173 runmat_accelerate::simple_provider::register_inprocess_provider();
2174 let provider = runmat_accelerate_api::provider().expect("test provider");
2175 let _thread_provider = runmat_accelerate_api::ThreadProviderGuard::set(Some(provider));
2176 let source = provider
2177 .upload_integer(&runmat_accelerate_api::HostIntegerTensorView {
2178 data: runmat_accelerate_api::HostIntegerDataView::U64(&[
2179 1,
2180 9_223_372_036_854_775_808,
2181 u64::MAX,
2182 ]),
2183 shape: &[1, 3],
2184 })
2185 .expect("upload integer source");
2186 let empty = Value::Tensor(Tensor::new(Vec::new(), vec![0, 0]).expect("empty"));
2187 let plan = IndexPlan::new(vec![1], vec![1, 1], vec![1], 1, vec![1, 3]);
2188
2189 let Value::GpuTensor(updated) =
2190 block_on(delete_gpu_slice_with_plan(&source, &plan, &empty)).expect("delete")
2191 else {
2192 panic!("expected gpu tensor");
2193 };
2194 assert_eq!(
2195 runmat_accelerate_api::handle_integer_type(&updated),
2196 Some(runmat_accelerate_api::IntegerElementType::U64)
2197 );
2198 let host = block_on(provider.download_integer(&updated)).expect("download updated");
2199 assert_eq!(host.shape, vec![1, 2]);
2200 assert_eq!(
2201 host.data,
2202 runmat_accelerate_api::HostIntegerDataOwned::U64(vec![1, u64::MAX])
2203 );
2204 }
2205
2206 #[test]
2207 fn complex_rhs_view_shape_mismatch_reports_identifier() {
2208 let rhs = Value::ComplexTensor(
2209 ComplexTensor::new(vec![(1.0, 0.0), (2.0, 0.0), (3.0, 0.0)], vec![1, 3])
2210 .expect("complex tensor"),
2211 );
2212 let err = match build_complex_rhs_view(&rhs, &[2, 2]) {
2213 Ok(_) => panic!("shape mismatch should fail"),
2214 Err(err) => err,
2215 };
2216 assert_eq!(err.identifier(), Some("RunMat:ShapeMismatch"));
2217 }
2218
2219 #[test]
2220 fn complex_rhs_view_invalid_rhs_type_reports_identifier() {
2221 let rhs = Value::String("x".to_string());
2222 let err = match build_complex_rhs_view(&rhs, &[1]) {
2223 Ok(_) => panic!("non-numeric rhs should be rejected"),
2224 Err(err) => err,
2225 };
2226 assert_eq!(err.identifier(), Some("RunMat:InvalidSliceAssignmentRhs"));
2227 }
2228
2229 #[test]
2230 fn complex_rhs_view_reads_all_typed_integer_classes_without_f64_mirrors() {
2231 macro_rules! assert_typed_rhs {
2232 ($storage:ident, $real:expr, $imag:expr) => {{
2233 let rhs = ComplexTensor::new_integer(
2234 IntegerComplexStorage::new(
2235 IntegerStorage::$storage(vec![$real]),
2236 IntegerStorage::$storage(vec![$imag]),
2237 )
2238 .expect("typed integer components"),
2239 vec![1, 1],
2240 )
2241 .expect("typed complex rhs");
2242
2243 let ComplexRhsView::Tensor { data, .. } =
2244 build_complex_rhs_view(&Value::ComplexTensor(rhs), &[1])
2245 .expect("typed complex rhs view")
2246 else {
2247 panic!("complex tensor rhs must produce a tensor view");
2248 };
2249 assert_eq!(data, vec![($real as f64, $imag as f64)]);
2250 }};
2251 }
2252
2253 assert_typed_rhs!(I8, i8::MIN, i8::MAX);
2254 assert_typed_rhs!(I16, i16::MIN, i16::MAX);
2255 assert_typed_rhs!(I32, i32::MIN, i32::MAX);
2256 assert_typed_rhs!(I64, i64::MIN, i64::MAX);
2257 assert_typed_rhs!(U8, u8::MIN, u8::MAX);
2258 assert_typed_rhs!(U16, u16::MIN, u16::MAX);
2259 assert_typed_rhs!(U32, u32::MIN, u32::MAX);
2260 assert_typed_rhs!(U64, u64::MIN, u64::MAX);
2261 }
2262
2263 #[test]
2264 fn string_rhs_view_shape_mismatch_reports_identifier() {
2265 let rhs = Value::StringArray(
2266 StringArray::new(
2267 vec!["a".to_string(), "b".to_string(), "c".to_string()],
2268 vec![1, 3],
2269 )
2270 .expect("string array"),
2271 );
2272 let err = match build_string_rhs_view(&rhs, &[2, 2]) {
2273 Ok(_) => panic!("shape mismatch should fail"),
2274 Err(err) => err,
2275 };
2276 assert_eq!(err.identifier(), Some("RunMat:ShapeMismatch"));
2277 }
2278
2279 #[test]
2280 fn string_cell_rhs_view_rejects_shape_data_length_mismatch() {
2281 let data = ["a", "b", "c"]
2282 .into_iter()
2283 .map(|text| Value::String(text.to_string()))
2284 .collect();
2285 let rhs = Value::Cell(CellArray {
2286 data,
2287 shape: vec![2, 2],
2288 rows: 2,
2289 cols: 2,
2290 });
2291 let err = match build_string_rhs_view(&rhs, &[2, 2]) {
2292 Ok(_) => panic!("cell shape/data mismatch should fail"),
2293 Err(err) => err,
2294 };
2295 assert_eq!(err.identifier(), Some("RunMat:ShapeMismatch"));
2296 }
2297
2298 #[test]
2299 fn string_cell_rhs_uses_character_contents_not_display_layout() {
2300 let rhs = Value::Cell(CellArray {
2301 data: vec![
2302 Value::CharArray(runmat_value::CharArray::new_row("AvgOrders")),
2303 Value::CharArray(runmat_value::CharArray::new_row("AvgRevenue")),
2304 ],
2305 shape: vec![1, 2],
2306 rows: 1,
2307 cols: 2,
2308 });
2309 let view = build_string_rhs_view(&rhs, &[2]).expect("build string RHS view");
2310 let StringRhsView::Tensor { data, .. } = view else {
2311 panic!("cellstr RHS should materialize as a string tensor");
2312 };
2313 assert_eq!(data, vec!["AvgOrders", "AvgRevenue"]);
2314 }
2315
2316 #[test]
2317 fn string_rhs_view_invalid_rhs_type_reports_identifier() {
2318 let rhs = Value::Tensor(Tensor::new(vec![1.0], vec![1, 1]).expect("tensor"));
2319 let err = match build_string_rhs_view(&rhs, &[1]) {
2320 Ok(_) => panic!("non-string rhs should be rejected"),
2321 Err(err) => err,
2322 };
2323 assert_eq!(err.identifier(), Some("RunMat:InvalidSliceAssignmentRhs"));
2324 }
2325
2326 #[test]
2327 fn slice_acceleration_error_mapping_reports_identifier() {
2328 let err = map_acceleration_error("slice assign", "provider failed");
2329 assert_eq!(err.identifier(), Some("RunMat:AccelerationOperationFailed"));
2330 }
2331}