Repository navigation
MAT values that could not be read warn and are not shown as if they had been; copies and saves refuse what they cannot spell - #153
Merged
Conversation
…a field of a binary dictionary
A host hands an empty string in as {_type: 'string', _emptyDims}. Pasted
into a binary dictionary at an entry's value, a cell's element or a struct
field, the node writer wrote it as the bare tag Class="string"
Dimension="0*3"/>. MATLAB R2027a cannot read that tag at those places
("Invalid string returned."). The empty arm of
MatlabVariableNode._serializeArrayXml now writes the String bag's envelope
for it, at its dims without trailing 1s. That is the envelope
_serializeStringXml writes for the bag itself, and the envelope the
property writer already writes for this same value. The binary reader's own
empty is still written as it was read. An N-D one is left as it was; MATLAB
has not been asked about it.
The existing probe was re-run on core's new saves
(probe_host_string_empties_binary.m, R2027a). Only the nine host rows at
entry, cell and field changed. Each now has the bag's spelling and reads
back as the string it is. The refreshed truth is committed.
hostStringEmptiesBinary.test.ts: one existing test changes because it
pinned the old spelling as a control. A new test covers the trailing-1
case, and a new control covers the N-D and reader-held cases.
…code refuses its copy, as MATLAB measured A .mat file's (or a model workspace's) 1x1x2 cell or struct whose element 2 is a value the reader did not decode has a member with nothing to copy, as a rank-2 one has (F1-10). #138 left it copying as its whole MAT stream, with the member's own bytes in it, where the member is a sparse array whose values the file does not hold (F1-5). On main the copy is already refused: #132 made _matStream ask _matWritable after the encode, so serializeValue goes on to the member, which throws _undecodedRefusal. Nothing tested that. With the check narrowed to strings, the whole suite still passed, and the copies were again the streams #138's branch wrote, byte for byte. The struct members the reader did not decode were encoded into the stream as well. MATLAB was asked first (theme 2). nd_undecoded_copy_readback.mjs writes each 1x1x2 cell and struct as a .mat file, with a member whose values are absent, of a type with no width, or 2 of 4, plus a sparse array that decodes as the control. It pastes the stream each one copied as into a text and a binary dictionary through core's API. probe_nd_undecoded_copy_readback.m (R2027a) compares what `load` reads of the variable with what the dictionary reads of the pasted entry. 14 of the 16 match. The struct whose member's values are absent does not: it reads back from the text dictionary as a 1x0 double, and from the binary one not at all ("bad version or endian-key"). Nothing in the bytes tells that stream from one that reads back, so the copy stays refused, naming the member. Tests (test/undecodedCopy.test.ts): - for each of the 6 kinds F1-10 lists and the 3 unread sparse arrays, at element 2 of a 1x1x2 cell and struct out of a .mat and a model workspace: the copy is refused with the member's own not-decoded refusal (entry, path c{2} or s(2).f, reason), and there is nothing to paste into either dictionary; - control: the same cell and struct holding a sparse array that decodes copy as their stream and paste as what they are; - the truth: the generator, run on the source, makes the streams MATLAB was asked about; exactly the two absent-struct rows differ from `load`; the copy of each case but the control is refused. With _matStream's check narrowed to strings, 6 of these fail, each because the copy returned a stream instead of refusing. No source change, so dist is unchanged. No existing test changed.
…its save after a sibling edit
A struct whose field f is a value the reader did not decode has a member with nothing to write but
its placeholder (F1-10). Where such a struct takes an edit at a sibling and is then saved, the save
has to be refused at f, naming it, and nothing written in its place. On main it is, but nothing
tested it.
Where a user can make that edit: a text dictionary keeps a struct it read whole from a MAT stream
read-only (C-012), and a binary one its own hex. So the struct is one a text dictionary held, pasted
into a binary dictionary, which has no spelling for f. It takes two routes there:
- read from the text dictionary's JSON, with f a cdata: the binary writer reaches f through
StructNode and the XML arm of MatlabVariableNode, which refuses it;
- read whole from a MAT stream: the struct's XML arm writes it through _serializeStructValue, and
f's serializeValue refuses it. Unedited, a struct like this with a sparse member is written as
the hex of its stream as read; edited, it is refused at f.
In the dictionary the struct was read from, the same edit saves with f as its own cdata or hex.
Tests (test/undecodedCopy.test.ts):
- for each of the 6 kinds F1-10 lists and the 3 unread sparse arrays, at f of a text dictionary's
struct {f, g}, from its JSON and from a MAT stream, pasted into a binary dictionary: g takes an
edit, the save is refused with f's not-decoded refusal (entry "pasted", path pasted.f, final),
the next save meets the same refusal, and the edit stands;
- control: the same edit in the text and the binary dictionary the struct was read from saves, with
f as read and g as edited.
Each new case was checked against a mutant of the source that the rest of the suite survives:
- the XML arm refusing only at an entry: the 9 cases fail, each because the save went through.
It wrote f as the unescaped placeholder text, which reopened as 0, with g lost;
- serializeValue not refusing in a binary dictionary: the same 9 fail, on the stream route;
- an edited struct refusing a member that is its own cdata or hex: the control fails.
No source change, so dist is unchanged. No existing test changed.
… or save is refused A sparse array whose indices place non-zeros the file holds no values for is shown as `<3x4 sparse double, not decoded>` (F1-5), and its copy or save is refused (F1-10). The refusal said "S is a 3x4 double that was not decoded": a full array, which the file does not hold, so the message and the row disagreed. MatlabVariableNode._undecodedRefusal spelled the class from the node, which holds no non-zeros for such an array and so does not know it is sparse. It now adds "sparse" when the variable as read is sparse (MatVariable.isSparse), as MatParser spells the row. A full array the reader did not decode is named as before. Tests: - test/sparseUnreadValues.test.ts: a new case opens a .mat holding an unread sparse logical, complex double and single, one as a struct field and one as a cell element, and a full double the reader did not decode. Each sparse one's copy is refused naming "a 3x4 sparse <class>", as its row reads; the full one is still "a 3x4 double". - test/undecodedCopy.test.ts: the three unread sparse arrays in the N-D cell and struct copy cases and the sibling-edit save cases now check the whole refusal message, not just its ends. Existing assertions changed, each because it pinned the old wording of a sparse array: - test/sparseUnreadValues.test.ts, the session case: "a 3x4 double" becomes "a 3x4 sparse double"; - test/undecodedCopy.test.ts and test/sparseFixtures.test.ts, the spTall node built with no stream: "a 10000000x2 double" becomes "a 10000000x2 sparse double".
…ecoded, and say so
The MCOS decoder resolved each slot of a cell held by an object, and kept a
slot it could not read as undefined: an element that is not a matrix, one the
MAT reader recorded without decoding (more elements declared than its bytes
hold), or one of a class no arm spells. The node layer built that as the
double 0. So holder.C showed {0, [3+4i 5+6i]}, the 0 editable, with no
warning, and a copy of the slot was 0.
Each such slot is now shown as not decoded, in its own place: the MAT
reader's placeholder for a value it recorded without decoding, its class
and size for any other (<1x1 unknown, not decoded>), and
<unknown, not decoded> for an element that is not a matrix. It has no
editor, and a copy of it is refused as not decoded, naming its path.
It is also reported, in the words the same cell gets at the top of a .mat:
"holder.C{1}" was not decoded: <reason>. for a value the reader recorded
without decoding, and otherwise "holder.C" declares 2 cell(s) and only 1
could be read. A cell that also holds fewer cells than it declares gets one
warning that counts both.
The decoder records these slots in a side table keyed by the cell's value,
so the value a copy replays is unchanged. No MATLAB-written fixture has
such a slot.
dist is rebuilt from src.
…of failing the open The MAT reader reads a numeric part as one JS array, one number per element. V8 holds no more than 2^27 elements in one array (measured, Node 24): past that, push threw "RangeError: Invalid array length", nothing caught it, and the whole .mat failed to open, the variables beside it included. A 200 MB uint8 array was enough. Past MAT_ARRAY_ELEMENT_LIMIT (2^27) elements in one part, the array is now recorded as the reader's other refusals are: its class and size, a placeholder value, the reason, a part-unreadable warning naming it, and the bytes it was read from. Its row has no editor and its copy is refused as not decoded. The rest of the file reads as it did. The limit counts what the bytes hold, so an array of 2^27 elements or fewer reads as before. The same check covers a complex array's imaginary part, and a sparse array's row indices and column starts, which are read the same way. A setter for the limit, not in the package barrel, lets tests use a small limit, as the inflate ceiling's does. test/matElementLimit.test.ts has one case at the real size and the rest under a limit of 6: .mat variables, compressed records, struct fields, cell elements, sparse arrays, dictionary values (an untouched save writes them back as read) and model workspaces.
…coded, and say so
MATLAB writes every field of a struct as a matrix, so a field slot holding
any other data element is a damaged file. In a struct array the reader
already records the whole array as not decoded, with the reason. In a 1x1
it stepped over the slot and left the field out: the struct showed its
other fields only, the file gave no warning, and a copy of the struct had
no such field.
The field is now kept, in its place, as a value of no class or size the
bytes say, shown as <unknown, not decoded> (the spelling the MCOS decoder
already uses for a cell slot that is not a matrix). It has no editor. The
.mat reports it under its MATLAB path, for example:
"s.a" was not decoded: it is not a matrix (a data element of type 9).
It keeps no bytes, because there is no matrix to write it from. So its
copy, and the copy of the struct holding it, are refused as not decoded,
naming s.a. A tag that the end of the bytes cuts is still read as a cut,
as before.
Tests:
- test/matParser.test.ts: the case "skips a field slot whose element is
not a matrix" pinned fields {}, the behaviour this removes. It now
expects the field, not decoded, and one warning. It also covers the
small-element form, nesting in a struct, a struct array element and a
cell, and a new control for a 1x1 cut inside a field tag.
- test/matStructArray.test.ts: a new MatNode case. The struct shows both
rows, with a not decoded and b still 2. The source has one
part-unreadable warning. The copies are refused at s.a. The variables
around it are unchanged.
dist is rebuilt from src.
…oded
A value whose flags word names a class the reader does not model (255,
18) holds nothing the reader read. A complex one already shows as not
decoded: the node builders fail on it and what holds it catches that. A
real one did not fail, so it fell to the numeric arm and was built from
the null it was left as. It showed null (a 1x3 [null], an empty [ ]),
with an editor, of class unknown, and a commit turned it into a double.
That happened at the top of a .mat or a model workspace, and in a cell or
a struct anywhere, including inside a binary dictionary's stream.
It is now shown as what the file says of it, its class and size, not
decoded: <1x1 unknown, not decoded>, with no editor and no rows. Its copy
is refused, naming it, and the copy of a cell or struct holding it is
refused at it (c{1}, s.a). A .mat still reports it once, with the
reader's own warning. A model workspace still does not warn.
A complex one keeps the path it has: what holds it shows the whole value
as not decoded, and a dictionary says why.
Tests: test/unmodelledClassVariable.test.ts, a new B-21 block. It covers
both words on a .mat scalar, a 1x3 and an empty one, a cell element and a
struct field, the same in a model workspace, and a cell inside a binary
dictionary's stream. No existing test changed.
dist is rebuilt from src.
…elements
A .mat struct array or cell whose bytes hold fewer elements than it
declares (a 1x3 struct holding 2, a 1x3 cell holding 1) opens with a
warning and a row for each element held. Its copy is the declared size
beside only those elements. MATLAB R2027a was asked whether it reads
those copies as the padded value core shows, for 12 shapes, each pasted
into a text and a binary dictionary. `load` refuses every damaged file.
MATLAB reads each copy of an array holding SOME of its elements back at
its declared size, with the field names kept, the held elements as they
are, and the rest []. That holds at rank 2 and N-D, and for a struct cut
inside an element. Those copies are kept, and the truth file pins them.
It does not read back a rank-2 array holding NONE of them. The struct
array's bag named no field (its field names come from its element rows),
and MATLAB read a 1x3 struct with no fields in both formats. The cell's
XML was Dimension="0*0", which MATLAB read as a 0x0 cell. Those writes are
now refused with a final not-decoded MatBudgetError, as a value the
reader did not decode is: "Cannot write "c": c is a 1x3 cell none of
whose 3 elements could be read, ...". That covers the copy, the save into
a binary dictionary (a text dictionary's cdata of one, pasted), and the
copy of a cell or struct holding one, at its path (c{2}, s.f). The MAT
stream, which MatWriter writes padded, is not refused, so an N-D one
still copies.
Tests: test/shortArrayCopy.test.ts (new), with the probe
test/parity/matlab/probe_short_array_copy_readback.m, its generator
short_array_copy_readback.mjs and its truth file. No existing test
changed.
dist is rebuilt from src.
No test covered a 1x1 .mat cell whose bytes hold none of the element it
declares. Changing _heldNone's cell arm from `count > 0` to `count > 1`
left the whole suite green, yet it changes what is written: the copy
becomes `{Cell,[1,1],[]}` and the binary dictionary's XML
`Class="cell" Dimension="0*0"`, the spelling MATLAB was measured to read
as a 0x0 cell. The `whose one element could not be read` wording of
_heldNoneRefusal was never run.
shortArrayCopy.test.ts now opens such a cell, at the top level and as an
element of a 1x2 cell, and pastes a text dictionary's cdata of it into a
binary dictionary. It pins the warning, and the refusal of each copy and
of the save, naming the path and that wording. Under the `count > 1`
mutant all three refusals fail, because nothing is thrown.
Test only; no src or dist change.
probe_short_array_copy_readback.m and its generator, short_array_copy_readback.mjs, were the one probe pair with no row in test/parity/matlab/README.md. The row says what they ask, what MATLAB answered, where they write ($SHORT_COPY_OUT), the truth file they made and the test that grades it. Docs only.
The refusal of a cell holding none of the elements it declares asks
for a cell that declares at least one. No test said so: with the
cell's test loosened from `count > 0` to `count >= 0`, the whole suite
stayed green, yet every empty cell read from a MAT stream ({}, 1x0,
0x3) had its copy refused, and so did any cell or struct holding one.
Add a control: a 0x0, a 1x0 and a 0x3 cell, alone, in a cell and in a
struct, open with no warning and copy as their empty bags; MATLAB's own
three empty cells in dispcat.mat copy as theirs; and a text
dictionary's cdata of each, pasted into a binary dictionary, saves with
its extents and reopens as it. Under the loosened test this one fails,
at the copy and at the save alike, and every other test passes.
The two dist source maps of MatlabVariableNode conflicted; dist is rebuilt from the merged src (rm -rf dist && npm run build).
This was referenced Oct 19, 2026
Merged
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.
Nine items from the group H audit, one commit each. Review added three commits on top: two tests (937e2cf, b447807) and a docs row (ddd41ec), plus a merge of main (e63c64c). Each makes a MAT value that could not be read show as not decoded, with a warning, instead of as a made-up value. Each also makes a copy or save refuse a value the writers cannot spell. Each commit has
distregenerated (rm -rf dist && npm run build) and passednpm run verifyon its own.Rulings: theme 1 (the group F ruling of 2026-10-18, group 2): "warn and show what was read; never refuse the file, never crash. Measure MATLAB first where the item says so; a save the writer cannot spell is refused rather than written wrong." Theme 2: follow MATLAB, measured first, and fix at the root cause. F1-10 (group F): a copy or save of a value the reader did not decode is refused.
MATLAB truth is from R2027a 27.1.0.3409468. The probes opened only files the generators wrote under /tmp.
B-04: a host's empty string in a binary dictionary (fc841c0)
{_type:'string', _emptyDims}at an entry, a cell element or a struct field was written as a bareClass="string" Dimension="0*3"/>. MATLAB reads that as "Invalid string returned.". With a trailing 1 it was0*3*1, a spelling MATLAB was never asked about.MatlabVariableNode._serializeArrayXmlhad no string case.DataNode._stringXml). This is the same output_serializeStringXmland the property writer give for that value, byte for byte.probe_host_string_empties_binary.m. Exactly 9 rows changed (the host rows at entry, cell and field). Each now reads back as astringof its size, and the other 41 rows are byte-identical. The truth file is refreshed.hostStringEmptiesBinary.test.ts. New: a trailing 1 is written as the rank-2 bag (before the fix it failed withDimension="0*3*1"). A control: the N-D empty and the reader's own empty are written as before. The truth test also checks the host rows.stringread-back. Before the fix it failed withInvalid string returned..B-11: an N-D cell or struct holding a not-decoded member (c275491)
_matWritable()check refuses the copy with the member's refusal. Nothing tested that: narrowing the check to strings left all 10177 tests green.nd_undecoded_copy_readback.mjs+probe_nd_undecoded_copy_readback.mtest 16 pasted copies. 14 read back asloadreads them. A struct whose member's values are absent does not: text reads1x0 double, and binary errors (bad version or endian-key).loadalso fills in values the file lacks. So the row's "otherwise" applies, and the refusal stays.undecodedCopy.test.ts. 9 cases (the 6 F1-10 kinds and 3 sparse kinds) at element 2 of a 1x1x2 cell and struct, from a .mat and from a model workspace. A control and the truth checks are included. With the check narrowed, 6 failed because the copy returned a stream.B-12: a struct with a not-decoded member, edited at a sibling and saved (5eba059)
undecodedCopy.test.ts. 9 kinds, each pasted both from the JSON and from a stream. g takes the edit, and the save is refused atpasted.f(not-decoded). A second save is refused the same way, and the edit stands. The control is the same edit in the source dictionaries, which saves. Fail-before used mutants. With the XML arm refusing only at an entry, the save wrote<2x3 double, not decoded>as f, which reopened as f = 0 with g lost. Not refusing in a binary owner fails the stream route. Two over-refusing mutants fail the control.B-13: the refusal says "sparse", as the row does (c314bf0)
<3x4 sparse double, not decoded>, but the refusal said "a 3x4 double"._undecodedRefusalnow addssparsewhen_matVar.isSparse, as MatParser spells the row.sparseUnreadValues.test.tscovers logical, complex, single, a field and a cell element, with a full-double control. B-11 and B-12 now check the whole message. 8 failed before the fix, each only on the message (true at that commit; at the head, 11 tests fail without the fix, because B-15's sparse case also reads this wording).sparseUnreadValues.test.ts:228, the one the row names: "a 3x4 double" is now "a 3x4 sparse double".undecodedCopy.test.ts:254-255andsparseFixtures.test.ts:610: the same sparse node, "a 10000000x2 double" is now "a 10000000x2 sparse double".B-14: a cell slot under an MCOS object that could not be read (7f27ca5)
holder.Cshowed an editable0, with no warning. A copy of the slot gave 0.McosParser.resolveValuemaps the slot toundefined.undefinedgoes into a side table keyed by the cell's bag, so what a copy replays does not change. The node builds that slot as not decoded, with no editor and a refused copy. A slot with no reason of its own gets the F1-4 warning ("holder.C" declares 2 cell(s) and only 1 could be read.). A slot that has a reason is reported per slot, as before.undefined, because they share the root and the symptom.mcosShortValues.test.ts, 4 cases plus an identity control (5 tests). 4 failed before the fix, on the warning.nullin the slot. Text writes[ ], and binary refuses it. A struct field or a property under an object has the same root (from reading the code). Each is its own item. A copy ofholder.Citself (the cell, not the whole holder) also replaysnullin the slot. It pastes into both text and binary as{[ ], [3+4i 5+6i]}, and binary does not refuse it. So a slot that now shows<unknown, not decoded>copies as[ ]. main behaves the same, so this is not a regression. Follow-up under F1-10: refuse the copy of a bag that holds an unread slot.B-15: a MAT numeric array too large to hold no longer fails the open (f460fd3)
parseMatandaddMatSourcethrowRangeError: Invalid array length, which failed the whole open.readNumericArraypushes one JS number per element, and V8 throws past 2^27 elements (measured on Node 24).MAT_ARRAY_ELEMENT_LIMIT = 2 ** 27(with a test seam,setMatArrayElementLimit), the array is recorded as not decoded, with a reason. The existing machinery then gives the warning, keeps the bytes, withholds the editor and refuses the copy. Sparse row indices and column starts get the same check. Because the limit is 2^27, only arrays that throw today change.matElementLimit.test.ts, 11 cases: the real size, at and past a small limit, complex, compressed, sparse, nested, a dictionary's cdata and hex, a model workspace, and the seam set back. All 11 failed before the fix: the real size with the RangeError, the rest because the value was decoded. That holds with the test seam kept. A plain revert removes the seam, and 9 cases are then skipped.B-20: a 1x1 struct's field slot that is not a matrix (dda09c3)
['b'], warnings[]).MatParser.parseMatrixcovered only struct arrays.unknown, with the reason "it is not a matrix (a data element of type T)".collectUndecodedwarns, and the copy is refused. A cut tag is still read as a cut.matStructArray.test.ts(rows, warning, refused copies, neighbours) and a cut control inmatParser.test.ts. Both new cases failed before the fix, on the missing field.matParser.test.ts"skips a field slot whose element is not a matrix" pinnedfields {}, exactly what the ruling removes (the row names it). It now checks that the field is recorded, not decoded, with one warning. A comment in the struct-array test was updated too; no assertion there changed.0(probed on main). This is B-14's twin for struct fields and its own item.B-21: a real MAT value of a class the reader does not model (b5ae3a0)
nullwhere the file holds the information). It matches Damaged MAT input warns and shows what was read, instead of shifting values, overflowing the stack or opening as editable text #126's complex case.null, and a Value edit turned it into a double. In a cell or a struct it showednulltoo.parseMatVariable, a realunknownbecomescreateNotDecoded. It has no editor, and its copy is refused ("its MAT array class is not one this reader models").unknown. That literal change broke 12 tests that pin Damaged MAT input warns and shows what was read, instead of shifting values, overflowing the stack or opening as editable text #126's ruled complex case (the whole value not decoded, with a dictionary warning) and one B-20 test. So the arm comes after theundecodedarm and covers real values only.unmodelledClassVariable.test.ts, 6 tests: the scalar test runs once for each of 2 flag words, and 4 other tests cover a 1x3, a 0x0, a cell element, a struct field, a model workspace and a binary dictionary stream. All 6 failed before the fix, on editablenullrows. Dropping!isComplexfails the 12 Damaged MAT input warns and shows what was read, instead of shifting values, overflowing the stack or opening as editable text #126 tests.unknownshould also be not decoded at the member level. That needs a decision on how a dictionary warns for a nested member.B-26: copies of a short .mat struct array or cell (c507b2d)
Dimension="0*0"in binary.short_array_copy_readback.mjs+probe_short_array_copy_readback.mtest 12 shapes.loadrefuses all 10 damaged files. 21 of 24 copies read back as the padded value core shows (one of the 21 iscell_nonein text: it reads back padded, a 1x3 cell of[], where core shows{}. It is refused anyway, because a copy cannot know where it will be pasted, and in binary it is written0*0, which reads as 0x0). The 3 that do not are struct-holding-none (text and binary), which read with NO fields, and cell-holding-none in binary, which reads as 0x0.not-decodedMatBudgetError ("none of whose 3 elements could be read"). The commit message says "It does not read back a rank-2 array holding NONE of them"; that is true only for the struct, and for the cell in binary (the commit cannot be amended, so this body says so). InserializeValuethe check comes after the stream arm, because N-D streams were measured to read back padded.shortArrayCopy.test.ts, 11 tests: the truth (24 fragments, 3 differing rows pinned), top-level and nested refusals, the XML paste route, an N-D pin and a binary pin, plus the 1x1 test and the empty-cell control from the fix rounds below. 4 failed before the fix, each because the copy or save went through. With B-26's src reverted at the head, 5 tests fail, all inshortArrayCopy.test.ts: those 4 and the 1x1 test. The empty-cell control passes on the revert, as a control should.{Cell,[1,3],[]}bag pasted into binary still writes0*0. The audit's "refuse every short array" was not built, because MATLAB reads those copies as padded.Fix round 1 (937e2cf, ddd41ec)
_heldNone's cell arm fromcount > 0tocount > 1passed the whole suite. Under that mutant the copy is{Cell,[1,1],[]}and the binary XML isClass="cell" Dimension="0*0", the spelling MATLAB was measured to read as a 0x0 cell. A new test inshortArrayCopy.test.tscovers three paths: the top-level cell, the cell as element 2 of a 1x2 cell, and a text dictionary's cdata of it pasted into a binary dictionary. It checks the warning ("c" declares 1 cell(s) and only 0 could be read.), and checks that the copy, the holder's copy and the save are each refused with "a 1x1 cell whose one element could not be read", naming the path. Under the mutant all three refusals fail, because nothing is thrown. Under the mutant the 1x1 cell's binary copy is<P Name="Value" Class="cell" Dimension="0*0"/>. That is byte for byte the measured cell_none_binary fragment, which MATLAB R2027a reads as a 0x0 cell. No src or dist change.test/parity/matlab/README.mdnow has a row forprobe_short_array_copy_readback.mandshort_array_copy_readback.mjs, as it has for every other probe. The row says "so those copies are refused". For the cell, read that as "in both venues": a copy cannot know where it will be pasted.Fix round 2 (b447807, e63c64c)
b447807 (B-26, tests only): no test showed that a .mat cell declaring zero elements still copies. A mutant that changed
_heldNone's cell arm fromcount > 0tocount >= 0passed the whole suite (10235/10235). Under that mutant every empty cell read from a MAT stream ({}, 1x0, 0x3) has its copy refused as "a 0x0 cell none of whose 0 elements could be read", and so does the copy of any cell or struct that holds one. A new control inshortArrayCopy.test.tscovers:{_array_type: 'Cell', _dimensions, _elements: []};test/fixtures/display/dispcat.mat, which copy as their bags too;Dimension="0*0","1*0","0*3", also inside the cell and the struct), asemptyCellEntry.test.tspins for MATLAB's spelling, and the saved file reopens as that value.Under the mutant this test fails at the first copy, with the refusal above. Run on their own, the dispcat copies and all five binary saves fail under the mutant as well. Over the full suite the mutant fails exactly this one test, both before the merge (10235 others pass) and at e63c64c (10814 others pass). It changes no existing test and no src or dist.
e63c64c: merge of main. See Gates.
Follow-ups (outside the rows, and the same on main)
Under an MCOS cell, a logical or char slot that MatParser recorded without decoding still shows a made-up editable value, with no warning.
resolveValue's logical and char arms return a value, notundefined.A 1x1 .mat struct that declares a field but holds no value for it opens as a fieldless
<1x1 struct>, with no warning. Its copy drops the field.readOpaqueHandle(MatParser) still reads a handle's uint32 words with no element limit.A .mat struct array that declares fields but no elements (for example 0x0 or 1x0 with fields a and b) copies as
{_array_type: 'Struct', _dimensions, _elements: [{}], _fields: []}. In a binary dictionary it saves as<P Name="Value" Class="struct" Dimension="1*0"/>, without the<Field Name="a"/>that MATLAB writes (emptyStructEntry.test.ts). So its field names are lost. The analogous mutant on_heldNone's struct arm (count > 1tocount >= 0) also passes the whole suite. No test was added for it: a control would pin a copy that drops the fields. Whether to keep that copy or refuse it (theme 1) is for a later item.A 1x3 .mat cell holding none of its elements shows
{}, the 0x0 spelling (it warns); one holding 1 of 3 shows{1, [ ], [ ]}.Gates (at the head, e63c64c)
MatlabVariableNode.js.map,.d.ts.map). src andtest/parity/matlab/README.mdmerged automatically. main is merged in (e63c64c, a merge, not a rebase), and dist is rebuilt from the merged src withrm -rf dist && npm run build. A second rebuild at the head leaves the tree clean.npm run verifyat e63c64c is green: vitest 303 files passed, 1 skipped; 10815 tests passed, 27 skipped.DEX_READBACKS_CORPUS) pass 45/45 at e63c64c; with oldSaves.truth as well, 61/61 (round-3 review). NoDEX_MATLAB_CMDlive test reaches these changes.npm run verifygreen, 297 files passed, 1 skipped; 10236 tests passed, 27 skipped.Implements the maintainer's rulings, as audited in group H (2026-10-19).