matrixutil: implement horzcat/vertcat for all layouts, tighten row/co… - #146
Merged
Conversation
…l, de-allocate the mapRows/mapCols family horzcat/vertcat --------------- Both were `if isDenseColMajor then ... else ???`, so any row-major or strided operand threw NotImplementedError. Neither checked that the operands were conformable, and horzcat's fast path was `m.raw.appendedAll(m2.raw)` guarded only by `isDenseColMajor` - which does not imply `dataLength == numel`, so a leading-column submatrix view (dense by stride, but still carrying the parent's full backing array) spliced the parent's trailing columns into the result. Now layout-agnostic: both read each operand through its own `linearIndex` via `foreach2D`, so views, offsets and row-major inputs all work. The arraycopy fast path is retained but re-guarded on `hasSimpleContiguousMemoryLayout && isDenseColMajor`, which is the condition that actually makes it sound. Added the missing `rows`/`cols` conformability checks (MatrixDimensionMismatch). row / col --------- Not rewritten with foreach2D: that walks the whole matrix, which would turn these O(cols)/O(rows) reads into O(rows*cols) ones - foreach2D is for whole-matrix passes. Instead the per-element `m((i, j))` (a multiply-add plus a bounds check each step) becomes a hoisted base plus a single add per step, with an arraycopy when the line is contiguous. Bounds checking now validates only the line index, so an empty matrix still yields an empty array rather than throwing. mapRows / mapCols family ------------------------ Each iteration wrote back via `updateInPlace(Array[Int](idx), ::, ...)`, costing three array allocations per line: the one-element Array[Int] naming the line, plus - inside updateInPlace - `range(::, n)`, which is `Array.from((0 until n).toArray)` and so materialises the full index array twice. Replaced with writeRow/writeCol helpers doing the same strided walk as row/col, leaving only the unavoidable line copy that the `Array[A] => Array[B]` signature requires. They also now reject a function that changes the line length, instead of silently truncating or throwing an opaque AIOOBE. mapRowsToScalar/mapColsToScalar's hand-rolled isDenseRowMajor / isDenseColMajor branches are deleted: row/col now do exactly that arraycopy under the strictly weaker `colStride == 1` / `rowStride == 1`, so the special cases were both redundant and narrower than the general path they guarded. Tests cover horzcat/vertcat across all four fast/slow operand combinations, the submatrix-view regression, row/col over row-major / col-major / padded layouts, in-place mapping on non-column-major matrices, and the new conformability and line-length errors.
Bytecode audit (Tier 1) — ✅ cleanJDK 25.0.1 (major 25), OpenJDK 64-Bit Server VM Vector lanes (
none Annotated methods (180)
Method sizes
Proposed baseline{
"jdkMajor": 25,
"c9": { "totalBytes": 38578, "distinctOps": 205 },
"annotated": {
"vecxt.NDArrayDoubleOps$.binaryOpGeneral(Lvecxt/ndarray$NDArray;Lvecxt/ndarray$NDArray;Lscala/Function2;)Lvecxt/ndarray$NDArray;": 186,
"vecxt.NDArrayDoubleOps$.binaryOpInPlaceGeneral(Lvecxt/ndarray$NDArray;Lvecxt/ndarray$NDArray;Lscala/Function2;)V": 153,
"vecxt.NDArrayDoubleOps$.compareGeneral(Lvecxt/ndarray$NDArray;Lvecxt/ndarray$NDArray;Lscala/Function2;)Lvecxt/ndarray$NDArray;": 186,
"vecxt.NDArrayDoubleOps$.compareScalarGeneral(Lvecxt/ndarray$NDArray;DLscala/Function2;)Lvecxt/ndarray$NDArray;": 157,
"vecxt.NDArrayDoubleOps$.unaryOpGeneral(Lvecxt/ndarray$NDArray;Lscala/Function1;)Lvecxt/ndarray$NDArray;": 152,
"vecxt.NDArrayFloatOps$.binaryOpGeneral(Lvecxt/ndarray$NDArray;Lvecxt/ndarray$NDArray;Lscala/Function2;)Lvecxt/ndarray$NDArray;": 195,
"vecxt.NDArrayFloatOps$.binaryOpInPlaceGeneral(Lvecxt/ndarray$NDArray;Lvecxt/ndarray$NDArray;Lscala/Function2;)V": 162,
"vecxt.NDArrayFloatOps$.compareGeneral(Lvecxt/ndarray$NDArray;Lvecxt/ndarray$NDArray;Lscala/Function2;)Lvecxt/ndarray$NDArray;": 195,
"vecxt.NDArrayFloatOps$.compareScalarGeneral(Lvecxt/ndarray$NDArray;FLscala/Function2;)Lvecxt/ndarray$NDArray;": 165,
"vecxt.NDArrayIntOps$.binaryOpGeneral(Lvecxt/ndarray$NDArray;Lvecxt/ndarray$NDArray;Lscala/Function2;)Lvecxt/ndarray$NDArray;": 186,
"vecxt.NDArrayIntOps$.binaryOpInPlaceGeneral(Lvecxt/ndarray$NDArray;Lvecxt/ndarray$NDArray;Lscala/Function2;)V": 153,
"vecxt.NDArrayIntOps$.compareGeneral(Lvecxt/ndarray$NDArray;Lvecxt/ndarray$NDArray;Lscala/Function2;)Lvecxt/ndarray$NDArray;": 186,
"vecxt.NDArrayIntOps$.compareScalarGeneral(Lvecxt/ndarray$NDArray;ILscala/Function2;)Lvecxt/ndarray$NDArray;": 156,
"vecxt.NDArrayIntOps$.unaryOpGeneral(Lvecxt/ndarray$NDArray;Lscala/Function1;)Lvecxt/ndarray$NDArray;": 152,
"vecxt.doublearrays$.$div([DDII[DI)V": 116,
"vecxt.doublearrays$.$minus$bang([D)V": 98,
"vecxt.doublearrays$.$minus$eq([DD)V": 90,
"vecxt.doublearrays$.$minus$eq([D[D)V": 26,
"vecxt.doublearrays$.$minus([DDII[DI)V": 116,
"vecxt.doublearrays$.$minus([D[D)[D": 24,
"vecxt.doublearrays$.$plus$eq([DD)V": 90,
"vecxt.doublearrays$.$plus([DDII[DI)V": 116,
"vecxt.doublearrays$.$times$eq([D[D)V": 88,
"vecxt.doublearrays$.$times$times$bang([DD)V": 95,
"vecxt.doublearrays$.abs$bang([D)V": 98,
"vecxt.doublearrays$.abs([D)[D": 108,
"vecxt.doublearrays$.acos$bang([D)V": 98,
"vecxt.doublearrays$.acos([D)[D": 108,
"vecxt.doublearrays$.asin$bang([D)V": 98,
"vecxt.doublearrays$.asin([D)[D": 108,
"vecxt.doublearrays$.atan$bang([D)V": 98,
"vecxt.doublearrays$.atan([D)[D": 108,
"vecxt.doublearrays$.cbrt$bang([D)V": 98,
"vecxt.doublearrays$.cbrt([D)[D": 108,
"vecxt.doublearrays$.clamp$bang([DDD)V": 180,
"vecxt.doublearrays$.cos$bang([D)V": 98,
"vecxt.doublearrays$.cos([D)[D": 108,
"vecxt.doublearrays$.cosh$bang([D)V": 98,
"vecxt.doublearrays$.cosh([D)[D": 108,
"vecxt.doublearrays$.cumsum$bang([D)V": 27,
"vecxt.doublearrays$.cumsum([D)[D": 15,
"vecxt.doublearrays$.dot([D[D)D": 23,
"vecxt.doublearrays$.exp$bang([D)V": 98,
"vecxt.doublearrays$.exp([D)[D": 108,
"vecxt.doublearrays$.expm1$bang([D)V": 98,
"vecxt.doublearrays$.expm1([D)[D": 108,
"vecxt.doublearrays$.fillLinspace([DDD)V": 133,
"vecxt.doublearrays$.fma$bang([DDD)V": 85,
"vecxt.doublearrays$.increments([D)[D": 110,
"vecxt.doublearrays$.log$bang([D)V": 98,
"vecxt.doublearrays$.log([D)[D": 108,
"vecxt.doublearrays$.log10$bang([D)V": 98,
"vecxt.doublearrays$.log10([D)[D": 108,
"vecxt.doublearrays$.log1p$bang([D)V": 98,
"vecxt.doublearrays$.log1p([D)[D": 108,
"vecxt.doublearrays$.meanAndVariance([D)Lvecxt/MeanAndVariance;": 9,
"vecxt.doublearrays$.norm([D)D": 13,
"vecxt.doublearrays$.productSIMD([D)D": 86,
"vecxt.doublearrays$.sin$bang([D)V": 98,
"vecxt.doublearrays$.sin([D)[D": 108,
"vecxt.doublearrays$.sinh$bang([D)V": 98,
"vecxt.doublearrays$.sinh([D)[D": 108,
"vecxt.doublearrays$.sqrt$bang([D)V": 98,
"vecxt.doublearrays$.sqrt([D)[D": 108,
"vecxt.doublearrays$.sumSIMD([D)D": 85,
"vecxt.doublearrays$.sumSIMD([DII)D": 102,
"vecxt.doublearrays$.tan$bang([D)V": 98,
"vecxt.doublearrays$.tan([D)[D": 108,
"vecxt.doublearrays$.tanh$bang([D)V": 98,
"vecxt.doublearrays$.tanh([D)[D": 108,
"vecxt.doublearrays$.unary_$minus([D)[D": 108,
"vecxt.doublearrays$.variance([DLvecxt/VarianceMode;)D": 10,
"vecxt.doublearrays.meanAndVariance([DLvecxt/VarianceMode;)Lvecxt/MeanAndVariance;": 9,
"vecxt.floatarrays$.$minus$bang([F)V": 93,
"vecxt.floatarrays$.$minus$eq([FF)V": 84,
"vecxt.floatarrays$.$minus$eq([F[F)V": 26,
"vecxt.floatarrays$.$minus([F[F)[F": 24,
"vecxt.floatarrays$.$plus$eq([FF)V": 84,
"vecxt.floatarrays$.$plus$eq([F[F)V": 24,
"vecxt.floatarrays$.$times$eq([FF)V": 78,
"vecxt.floatarrays$.$times$eq([F[F)V": 84,
"vecxt.floatarrays$.$times$times$bang([FF)V": 85,
"vecxt.floatarrays$.abs$bang([F)V": 93,
"vecxt.floatarrays$.abs([F)[F": 104,
"vecxt.floatarrays$.acos$bang([F)V": 93,
"vecxt.floatarrays$.acos([F)[F": 104,
"vecxt.floatarrays$.asin$bang([F)V": 93,
"vecxt.floatarrays$.asin([F)[F": 104,
"vecxt.floatarrays$.atan$bang([F)V": 93,
"vecxt.floatarrays$.atan([F)[F": 104,
"vecxt.floatarrays$.cbrt$bang([F)V": 93,
"vecxt.floatarrays$.cbrt([F)[F": 104,
"vecxt.floatarrays$.clamp$bang([FFF)V": 172,
"vecxt.floatarrays$.cos$bang([F)V": 93,
"vecxt.floatarrays$.cos([F)[F": 104,
"vecxt.floatarrays$.cosh$bang([F)V": 93,
"vecxt.floatarrays$.cosh([F)[F": 104,
"vecxt.floatarrays$.cumsum$bang([F)V": 27,
"vecxt.floatarrays$.cumsum([F)[F": 15,
"vecxt.floatarrays$.dot([F[F)F": 23,
"vecxt.floatarrays$.exp$bang([F)V": 93,
"vecxt.floatarrays$.exp([F)[F": 104,
"vecxt.floatarrays$.expm1$bang([F)V": 93,
"vecxt.floatarrays$.expm1([F)[F": 104,
"vecxt.floatarrays$.fma$bang([FFF)V": 80,
"vecxt.floatarrays$.increments([F)[F": 97,
"vecxt.floatarrays$.log$bang([F)V": 93,
"vecxt.floatarrays$.log([F)[F": 104,
"vecxt.floatarrays$.log10$bang([F)V": 93,
"vecxt.floatarrays$.log10([F)[F": 104,
"vecxt.floatarrays$.log1p$bang([F)V": 93,
"vecxt.floatarrays$.log1p([F)[F": 104,
"vecxt.floatarrays$.norm([F)F": 13,
"vecxt.floatarrays$.productSIMD([F)F": 82,
"vecxt.floatarrays$.sin$bang([F)V": 93,
"vecxt.floatarrays$.sin([F)[F": 104,
"vecxt.floatarrays$.sinh$bang([F)V": 93,
"vecxt.floatarrays$.sinh([F)[F": 104,
"vecxt.floatarrays$.sqrt$bang([F)V": 93,
"vecxt.floatarrays$.sqrt([F)[F": 104,
"vecxt.floatarrays$.sumSIMD([F)F": 81,
"vecxt.floatarrays$.tan$bang([F)V": 93,
"vecxt.floatarrays$.tan([F)[F": 104,
"vecxt.floatarrays$.tanh$bang([F)V": 93,
"vecxt.floatarrays$.tanh([F)[F": 104,
"vecxt.floatarrays$.unary_$minus([F)[F": 104,
"vecxt.intarrays$.$bang$colon$eq([II)[Z": 94,
"vecxt.intarrays$.$bang$colon$eq([I[I)[Z": 106,
"vecxt.intarrays$.$eq$colon$eq([II)[Z": 94,
"vecxt.intarrays$.$eq$colon$eq([I[I)[Z": 106,
"vecxt.intarrays$.$greater$eq([II)[Z": 94,
"vecxt.intarrays$.$greater$eq([I[I)[Z": 106,
"vecxt.intarrays$.$greater([II)[Z": 94,
"vecxt.intarrays$.$greater([I[I)[Z": 106,
"vecxt.intarrays$.$less$eq([II)[Z": 94,
"vecxt.intarrays$.$less$eq([I[I)[Z": 106,
"vecxt.intarrays$.$less([II)[Z": 94,
"vecxt.intarrays$.$less([I[I)[Z": 106,
"vecxt.intarrays$.$minus$eq([II)V": 67,
"vecxt.intarrays$.$minus$eq([I[I)V": 84,
"vecxt.intarrays$.$minus([II)[I": 16,
"vecxt.intarrays$.$minus([I[I)[I": 24,
"vecxt.intarrays$.$plus$eq([I[I)V": 84,
"vecxt.intarrays$.$plus([I[I)[I": 24,
"vecxt.intarrays$.countsToIdx([I)[I": 70,
"vecxt.intarrays$.dot([I[I)I": 108,
"vecxt.intarrays$.increments([I)[I": 121,
"vecxt.intarrays$.maxSIMD([I)I": 82,
"vecxt.intarrays$.mean([I)D": 11,
"vecxt.intarrays$.meanAndVariance([I)Lvecxt/MeanAndVariance;": 9,
"vecxt.intarrays$.minSIMD([I)I": 82,
"vecxt.intarrays$.std([I)D": 9,
"vecxt.intarrays$.std([ILvecxt/VarianceMode;)D": 10,
"vecxt.intarrays$.stdDev([I)D": 9,
"vecxt.intarrays$.sumSIMD([I)I": 81,
"vecxt.intarrays$.variance([I)D": 9,
"vecxt.intarrays$.variance([ILvecxt/VarianceMode;)D": 10,
"vecxt.intarrays.gt([II)[Z": 9,
"vecxt.intarrays.gt([I[I)[Z": 9,
"vecxt.intarrays.gte([II)[Z": 9,
"vecxt.intarrays.gte([I[I)[Z": 9,
"vecxt.intarrays.lt([II)[Z": 9,
"vecxt.intarrays.lt([I[I)[Z": 9,
"vecxt.intarrays.lte([II)[Z": 9,
"vecxt.intarrays.lte([I[I)[Z": 9,
"vecxt.intarrays.meanAndVariance([ILvecxt/VarianceMode;)Lvecxt/MeanAndVariance;": 9,
"vecxt.intarrays.stdDev([ILvecxt/VarianceMode;)D": 9,
"vecxt.matrix$Layout.linearIndex(II)I": 19,
"vecxt.matrix$Matrix.colStride()I": 8,
"vecxt.matrix$Matrix.cols()I": 8,
"vecxt.matrix$Matrix.hasSimpleContiguousMemoryLayout()Z": 8,
"vecxt.matrix$Matrix.isDenseColMajor()Z": 8,
"vecxt.matrix$Matrix.isDenseRowMajor()Z": 8,
"vecxt.matrix$Matrix.numel()I": 8,
"vecxt.matrix$Matrix.offset()I": 8,
"vecxt.matrix$Matrix.rowStride()I": 8,
"vecxt.matrix$Matrix.rows()I": 8,
"vecxt.ndarray$.mkNDArray(Ljava/lang/Object;[I[II)Lvecxt/ndarray$NDArray;": 13,
"vecxt.ndarray.shapeArray(Lvecxt/ndarray$NDArray;)[I": 8,
"vecxt.ndarrayOps.expandDims(Lvecxt/ndarray$NDArray;I)Lvecxt/ndarray$NDArray;": 9
}
}
|
mapRows reads a row at a time and writes a row at a time, but built its destination with Matrix(raw, rows, cols) — dense column-major — so every write strode by `rows`. After the previous commit the read side already arraycopies for a row-major source; making the destination row-major means writeRow arraycopies too, so mapRows on a row-major input is now contiguous on both ends. It also brings this in line with the layout policy the scalar ops document in doublematrix.scala (result orientation follows the operation's own axis) instead of unconditionally normalising to column-major. mapCols keeps column-major for the mirror reason. Guarded on zero dimensions: the strided factory goes through strideMatInstantiateCheck, which rejects rows/cols <= 0, whereas the (raw, rows, cols) factory permits them — without the guard an empty matrix would start throwing InvalidMatrix instead of mapping to an empty result. Two existing assertions compared `mapRows(...).raw` against a column-major expectation, i.e. they pinned storage order rather than logical content, so they fail on any layout change however correct. Both now compare via assertMatrixEquals, which indexes by (i, j) and is indifferent to either side's layout; the logical values they check are unchanged. Added tests asserting the two orientations explicitly, so a future change to either default has to be deliberate, plus a single-column case where row- and column-major coincide.
…das/vecxt into matrixutil-horzcat-vertcat
…Into The bytecode audit flagged four ScalaRunTime array_apply/array_update call sites — the generic fallback loops in horzcat and vertcat. Cause was mine: I had written those loops as per-element reads over `Array[A]` and, at the same time, demoted `vertcat` from `inline def` to a plain `def` (horzcat was already plain, but its old body only used appendedAll/arraycopy, which take Object wholesale and so never needed typed element access). With `A` abstract and the method not inline, `Array[A]` erases to Object and every element access compiles to ScalaRunTime — boxing, and per C6a's own notes no primitive array left for a vectorised path to fall back from. Scala 3 has no @specialized, so `inline` is the mechanism that recovers a concrete double[]/int[] per call site; it is why every other element-touching method in this file is already inline. Both are now `inline def`, which restores vertcat's original modifier. This specialises the access rather than merely silencing the check. Also folded the four near-identical copy branches into one private inline `blitInto(src, dest, destRows, rowOff, colOff)`. horzcat and vertcat differ only in which offset advances, so they are now two calls each. Its three paths (whole-matrix arraycopy / per-column arraycopy / strided linearIndex read) are the same ones as before, with the index arithmetic checked against the previous inline versions — including the degenerate zero-row/zero-column operands, which now take the whole-matrix path rather than a zero-trip column loop.
Quafadas
added a commit
that referenced
this pull request
Aug 11, 2026
…#149) * intarray: scaladoc all three methods, de-inline select and contiguous select and contiguous are now plain `def`s. Both extend Array[Int], so their reads and writes already compile to iaload/iastore with nothing for `inline` to specialise, and neither body folds to a constant — they hold loops. LongArrays.select is the same method over Array[Long] and was already a plain def, so this also removes an inconsistency. C9's ratchet names "something became `inline` that did not need to be" as a regression it watches for, and this moves that metric the right way: two loop bodies stop being copied into every call site. mask keeps `inline`, deliberately. Unlike its neighbours it is generic in A, so Array[A] erases to Object and all three of dimCheck(vec, index) (which reads vec.length), vec(i) and newVec(j) = _ compile to ScalaRunTime$ calls — array_length, array_apply, array_update — every one of them on C6a's banned list. Scala 3 has no @specialized, so `inline` is what recovers a concrete primitive array at the call site. This is the same line NDArrayCheck.scala already draws in its header comment: generic code that reaches into Array[A] keeps inline, generic code that only touches Array[Int] does not need it. Dropping it here would reproduce the C6a failure that horzcat/vertcat hit in #146. Scaladoc covers behaviour rather than restating the signature, in particular the edges that other code depends on: mask's result length is the number of trues; select is a gather, so indices may be permuted or repeated and the result length is unrelated to the source's; and contiguous is vacuously true for empty and single-element arrays, which is precisely what lets Matrix.submatrix and Matrix.apply(rowRange, colRange) take the zero-copy path for a single-index selection like mat(Array(1), ::). Also gave mask an explicit Array[A] return type, which was inferred. * [autofix.ci] apply automated fixes * intarray: annotate select and contiguous @hotpath, drop breakable Both carry a loop, so per the annotation docs the applicable budget is FreqInlineSize and the annotation is @hotpath, not @thin — @thin is for forwarders and explicitly forbids the backward branch these have. This is the same call intarrays.=:= documents for itself. Neither gets @AllocFree: select returns a fresh array by construction, which the annotation's own doc rules out ("a method returning a fresh array is not a candidate however hot it is"). mask stays unannotated. Its body is a per-element loop and would otherwise qualify, but check A1 fails an annotation on an `inline def` by name — an inline body is never emitted, so there is no bytecode for C2 to measure and the annotation would read as a guarantee nothing verifies. Here the two requirements are mutually exclusive: annotating it would mean dropping the `inline` that C6a requires for its generic Array[A] access. contiguous no longer uses scala.util.control.Breaks. That was not cosmetic. `breakable` takes its block by-name, so the loop compiled to a lambda and the `var`s it mutated were boxed into IntRef/BooleanRef to be captured — an allocation per call on a path every submatrix and every apply(rowRange, colRange) runs, to express an early exit the `while` condition states directly. It also left @hotpath with nothing to describe: the per-element work sat in the synthetic lambda rather than in this method's bytecode, so the annotation would have measured the wrong body. The condition-based loop is behaviour-identical; I checked empty, single-element, ascending, descending, stepped, repeated and break-at-the-last-pair against the old semantics, and added tests for the cases the scaladoc now claims — the empty and single-element ones being load-bearing for the matrix zero-copy path. * [autofix.ci] apply automated fixes --------- Co-authored-by: Simon Parten <simon.parten@sire.ch> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
…l, de-allocate the mapRows/mapCols family
horzcat/vertcat
Both were
if isDenseColMajor then ... else ???, so any row-major or strided operand threw NotImplementedError. Neither checked that the operands were conformable, and horzcat's fast path wasm.raw.appendedAll(m2.raw)guarded only byisDenseColMajor- which does not implydataLength == numel, so a leading-column submatrix view (dense by stride, but still carrying the parent's full backing array) spliced the parent's trailing columns into the result.Now layout-agnostic: both read each operand through its own
linearIndexviaforeach2D, so views, offsets and row-major inputs all work. The arraycopy fast path is retained but re-guarded onhasSimpleContiguousMemoryLayout && isDenseColMajor, which is the condition that actually makes it sound. Added the missingrows/colsconformability checks (MatrixDimensionMismatch).row / col
Not rewritten with foreach2D: that walks the whole matrix, which would turn these O(cols)/O(rows) reads into O(rows*cols) ones - foreach2D is for whole-matrix passes. Instead the per-element
m((i, j))(a multiply-add plus a bounds check each step) becomes a hoisted base plus a single add per step, with an arraycopy when the line is contiguous. Bounds checking now validates only the line index, so an empty matrix still yields an empty array rather than throwing.mapRows / mapCols family
Each iteration wrote back via
updateInPlace(Array[Int](idx), ::, ...), costing three array allocations per line: the one-element Array[Int] naming the line, plus - inside updateInPlace -range(::, n), which isArray.from((0 until n).toArray)and so materialises the full index array twice. Replaced with writeRow/writeCol helpers doing the same strided walk as row/col, leaving only the unavoidable line copy that theArray[A] => Array[B]signature requires. They also now reject a function that changes the line length, instead of silently truncating or throwing an opaque AIOOBE.mapRowsToScalar/mapColsToScalar's hand-rolled isDenseRowMajor / isDenseColMajor branches are deleted: row/col now do exactly that arraycopy under the strictly weaker
colStride == 1/rowStride == 1, so the special cases were both redundant and narrower than the general path they guarded.Tests cover horzcat/vertcat across all four fast/slow operand combinations, the submatrix-view regression, row/col over row-major / col-major / padded layouts, in-place mapping on non-column-major matrices, and the new conformability and line-length errors.