// Vertical concatenation: the rows of `other` appended
// under `self`'s, one frame on top of the other. The two frames must agree
// column-for-column **by position** — same column count, same name in each
// slot, same dtype in each slot. Silently stacking one column's values
// under another column's name is the bug the name check exists to prevent,
// so reordering `other`'s columns — or renaming them — is the caller's job
// before `vstack`. The dtype check is exact, not
// unifying: an `Int` column does not stack under a `Float` one; cast first
// when the sources differ. Row counts are unconstrained — stacking onto a
// 0-row frame is the schema-preserving way to "concat" — and the rebuilt
// columns re-converge onto the `Numeric` fast path exactly as any row
// gather's do, since the backend follows the assembled content.
///|
/// The rows of `other` appended under `self`'s, as one frame.
/// Raises `TypeMismatch` when the column counts
/// differ, when any same-position pair of columns carries different names,
/// or when any same-position pair carries different dtypes; the message
/// names the count or the first offending column.
pub fn DataFrame::vstack(
self : DataFrame,
other : DataFrame,
) -> DataFrame raise @types.DataError {
if self.ncols() != other.ncols() {
raise @types.DataError::TypeMismatch(
@types.TypeMismatchDetail::Message(
"vstack needs equal column counts: \{self.ncols()} vs \{other.ncols()}",
),
)
}
// The stacked height is the plain sum. A sum past 2^31 would wrap, but
// every column below materialises `self.nrows() + other.nrows()` cells
// first and allocation fails long before the count wraps — so there is
// deliberately no guard here, only the comment (a guard would be an
// untestable dead arm).
let height = self.nrows() + other.nrows()
let cols : Array[Series] = []
for i in 0..