Repository navigation
Nodes keep what they read and check every spelling of a property: JSON re-reads, one-element arrays, struct fields, bus elements, Complexity, Parameter shape - #152
Merged
Conversation
…om its text
A text dictionary's own entry that has no MAT stream MatWriter can write
(a hand-typed bag of an N-D cell, struct or string holding a string) opens
as what was read, and a save writes it back as read until it is edited.
Re-read from the dictionary's text with { asRead: true }, as a JSON view
re-reads an entry the user edited there, the same entry was refused as a
paste is (ruling A1), with "no MAT class for string". parseEntry now skips
that refusal for the re-read alone (F1-22, kept as read, not refused); a
paste, a drop and an undo record are refused as before, and the re-read
entry's save is refused once it is edited, as the opened entry's is.
…elements A one-element array of a class with a node of its own (ParameterNode, SignalNode, AliasTypeNode and every other typed class), and of a class with none (ObjectNode), writes its binary XML through DataNode._serializeSimulinkObjectXml. Since #134 that writer refuses dims no Dimension can spell, but it still wrote the one element as a scalar object whatever the dims counted: a Parameter pasted with _dimensions [2,1], [1,0], [1,2] or [0,0] and one element was saved as a 1x1 Parameter, its dims gone. The writer now refuses such an array, naming the entry, the path, the class, the dims and the one element (MatWriteError: this writer has no spelling for it). Every extent must be 1, as [2,0.5] multiplies to 1 too. [1,1], [1,1,1], [1], [] and no _dimensions are written as before. An array of more than one element is ObjectNode's to write, and one of none is left as it was. A text dictionary's save keeps such a value as pasted. The new dimsWriters case covers Parameter, Signal, AliasType and a class with no node of its own, as an entry, a cell entry's element and a struct entry's field, at the five dims, with the text-dictionary save and five scalar controls in each. dist is rebuilt.
…onary, in the struct's order
A struct copied into a binary dictionary at a Simulink.Parameter's Value, at
an object's property, or in a cell or struct field under those, is written
from its bag (DataNode._structXml). Each element was written with only the
fields it held, in its own key order: <Element></Element> for an element
that is {} or null. MATLAB R2027a read a one-element struct back with no
fields. It read a struct whose first element lacked a field, or held its
fields in another order than _fields, with its fields in another order.
(MATLAB's own text dictionaries sort the keys and keep the order in
_fields.) Core's binary reader read those saves the same way.
Each element now gets every field the struct has, as StructNode reads them
(_fields, or element 1's keys), in that order. A field the element does not
hold is written as [] (Dimension="0*0"), then any other field it holds, each
name once. This is what StructNode writes for the same struct.
Measured first. probe_struct_absent_fields_binary.m reopens 144 dictionaries
that struct_absent_fields_binary.mjs saves through core: nine structs at
four places, each written as held, in the new spelling, with the [] after
the held fields, and as held in a text dictionary. In every case MATLAB
reads the new spelling as it reads the text source, and the other two
spellings differ where the commit says. At a Parameter's Value, a struct
with an empty field is not a Value MATLAB holds: it reads both the text
source and the binary save as a double 0x0. A struct whose elements already
hold every field in order is written as before.
B-17 (group H): theme 2, F1-16.
…th a warning A text dictionary is JSON anyone can edit, so the list a bus's Elements_internal or an enumeration's Enumerals holds its elements in can be a number, a string, an object or a null. MATLAB never writes one. Read as a list, a number, a string or an object threw "forEach is not a function" and the whole dictionary failed to open. A null opened as an entry with no elements, with nothing said, and an untouched save rewrote the list as []. Now the entry shows no element, one part-unreadable warning against it says what was read where the list belongs, and an untouched save writes the entry as it was read (_keepNullElement), as a list holding a value that is not an element already does (C-190). An edit to the entry writes it from its rows, which are none: what the same edit writes once its elements are all deleted, and its binary XML is the XML of that entry. The null case goes beyond the item's row, which named the values that threw: it is the same hand edit under the same ruling (warn and show what was read), and it was silent.
A Signal's, a BusElement's and a ValueType's Complexity and DimensionsMode take only MATLAB's
enumerals (C-030). An edit can name the property by its column (complexity) or by the field
the writers save (Complexity). The prop lookup every per-property gate asks matched only the
key and the column, so setProperty('Complexity', 'bogus') found no prop: the enumeral check,
the unchanged-commit check and the Signal's own enum test all let it through, and 'bogus' was
saved into a file MATLAB then refuses. The same call with 'complexity' was refused.
The lookup (BaseNode's propNamed, used by DataNode._propFor) now also matches the prop's node
field, after the key and the column, and SignalNode checks the prop the name resolves to. So
the field spelling is refused in MATLAB's words, a value set back to what it shows is no edit,
and an accepted value saves the file the column spelling saves.
The session's capture of the value an edit replaces, for its undo, had the same gap: it found
no prop for 'Complexity', so the undo set the field to nothing and was refused. It now finds
the prop by the same rule, and the undo puts the old value back. This goes beyond the item's
row, which named the node's gates: it is the same lookup, and without it the session edit by
the field's name could not be undone.
…ctionary
MATLAB R2027a writes a bus of one element, in a text dictionary, with its Elements_internal as
the element itself, {_id, _object_class, _properties}, and a bus of more as the array,
{_array_class, _dimensions, _elements}. BaseBusNode rebuilt every Elements_internal as the
array, so a save that changed nothing changed the file: a no-op save of the MATLAB-written
params.sldd differed at byte 4731, where MyConnBus's _object_class became _array_class (A4, D3).
An Elements_internal read in the element's form is now written back in it while the bus holds
one element. That element is the one read, as read, or one added here with its class put in
after its _id. With more elements the bus is written as MATLAB writes the array, without the
element's own _object_class. A bag read as the array stays the array. The binary writer's
output is unchanged: it already spelled both forms the same.
Measured first (probe_bus_one_element_text.m, R2027a): MATLAB made a Bus, a ConnectionBus and
a ServiceBus of one element each, then edited copies, adding an element, removing it again, or
removing the first. After an add and a remove it writes the element form again (only lastmod
differs). Core makes the same edits through the session API (bus_one_element_text.mjs). Each
save's value is MATLAB's, except the _id: MATLAB numbers the elements again on its save, and
core keeps the _id it gave the element. MATLAB reads back each of core's five saves as it reads
its own file after the same edits, with no warning.
The baseBusNode.test.ts case that pinned the array form ("always emits the canonical
_array_class form") flips, as A4 allows. Its reason, that MATLAB would fail to load the file,
holds for _elements written under _object_class. This change does not write that.
…ry edit of it
A Simulink.Parameter kept the Dimensions and Complexity it was read with
after an edit of its Value, shown and saved. Cut from three elements to
two, pRow (complex_binary.sldd) still had [1 3]. With an element added it
still had [1 3]. With [1 2 3 4] typed over it, it had [1 3] and 'complex'.
The ruling A7 (2026-10-15) had fixed only a removal that collapses the
Value to a scalar.
MATLAB R2027a was asked first (test/parity/matlab/probe_parameter_shape.m,
41 rows on copies of the fixture, truth parameter_shape.truth.json). It
refuses a numeric Dimensions other than the Value's size, and any
Complexity set. After every edit of the Value, both are the Value's size
and complexity, with these exceptions:
- a symbolic Dimensions ('[1,N]') is kept;
- an empty Value keeps both;
- a struct Value keeps its Complexity;
- text is a string scalar: [1 1], real.
MATLAB also turns Complexity 'real' once every element is set real.
ParameterNode._followValueShape now sets both from the Value after each
of these edits: an Add, a removal, a Value typed on the Parameter, on its
Value row, or on one of its elements, and the undo or redo of each. Each
is set only where the file holds it. MatlabVariableNode tells what holds
an array when one of its elements is set, as it already does for an Add
or a removal. The undo of a typed Value puts back what the two held.
The undo of an Add or a removal sets them again from the Value it puts
back. This replaces the bookkeeping that worked only for the collapse.
MATLAB reopened core's save of each of the 36 edits core makes. It reads
the Dimensions and Complexity its own edit leaves in all but one. That
one comes from an older defect in how the Value is written: a complex
array whose elements are all real now is written with no Dimension. It
is named in parameter_shape.steps.json and left for its own fix.
Tests: test/parameterShape.test.ts (new, 87 cases) makes each row
through the session on the same entry and file. It also covers undo and
redo, a text dictionary's Parameter, and MATLAB's reads of core's saves.
No existing test changed.
…its file does not hold After an edit of its Value, a Simulink.Parameter's Dimensions and Complexity are set from the Value only where the file holds them (ParameterNode._followValueShape). No test held that: either guard dropped passed the full suite, and such a save writes the Value's shape into a Parameter that never carried it. MATLAB writes binary Parameters with Dimensions and no Complexity, and core's own text dictionaries hold Parameters with no Dimensions. The new tests take complex_binary.sldd with pRow's Complexity, or its Dimensions, taken out, make five of the probe's edits through the session, and check that the save and its reopen hold no such key, and the other key as MATLAB's own edit leaves it. Two text cases do the same: typeLink.sldd Kp, which holds no Dimensions, and typed_mcos.sldd pDoubleRow with its Complexity taken out. With either guard dropped, 7 of the 14 new tests fail, each on the key that guard writes. Test only: src and dist are unchanged.
… no field in a binary dictionary
A text dictionary's `_fields` is JSON anyone can edit, so a member of it
can be a number or null. DataNode._structXml writes such a member as no
field, as main did, since main wrote only the fields an element holds.
No test held that: with the string check dropped, the full suite
passed, and such a save either fails (the name is not a string) or
writes a field `<P Name="5">` that no MATLAB struct can have.
The new test pastes a struct over {a: 1} whose `_fields` is ['a', 5] or
[null, 'a'] at each of the four places, into MATLAB's binary fixture.
It checks that the save is the spelling of the same struct with only
`a` (C1 held), which MATLAB reads as a struct with field `a`, and that
it reopens with `a = 1`. With the string check dropped, all four fail
because the save throws. With the name written as String(field), all
four fail on the extra `<P Name="5">`.
Test only: src and dist are unchanged.
The comment over the B-25 lock-down tests said MATLAB itself writes binary Parameters with Dimensions and no Complexity. R2027a's own edit writes both keys; it is MATLAB-written dictionaries that can lack Complexity. Comment only.
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.
Seven node fixes, one commit each, plus two test-only commits from review (see "Review fix round 1" below). Every commit has dist rebuilt and committed, and
npm run verifypassed at each one.B-09: a text dictionary's own value is kept as read when a host re-reads it
Ruling: F1-22, "kept as read, not refused". The open follow-up said: "a re-read from the JSON view still ... runs the
_unwritableStreamrefusal". Theme 1 also applies: a save the writer cannot spell is still refused.What was wrong: a text dictionary's own 2x2x2 cell holding a string opened fine. The
{ asRead: true }re-read then threwMatWriteError ... no MAT class for "string".Root cause:
SectionNode.parseEntryran_unwritableStream()on every text entry and never checkedoptions.asRead.Fix: the refusal is skipped when
asReadis set. The metadata stamp is left alone: it is an edit's stamp, the same oneaddEntrymakes.Tests (
reparseAsRead.test.ts):Changed existing test: none. The file's
textDictionaryhelper now takes its entry list as a parameter, and the default is unchanged.Left out: the paste refusal of a text N-D string calls the value a "char". That is a wording issue, outside this row.
B-16: refuse a one-element object array whose dims count a different number of elements
Ruling: group F group 1, "refusing what the writers cannot spell", and theme 1, "a save the writer cannot spell is refused rather than written wrong". The open follow-up (#134) said: "a one-element array with dims that can be spelled but are not scalar (
[1,0],[2,1]) is still written as a scalar."What was wrong: a pasted
{_array_class, _dimensions:[2,1], _elements:[one]}was written to a binary dictionary as a bare<Element Class=...>. Other dims did the same: [1,0], [1,2] and [0,0].Root cause:
DataNode._serializeSimulinkObjectXmlonly checked that the dims could be spelled. It never checked that the dims matched the number of elements.Deviation from the row: the row says ObjectNode already refuses this. It does not when only the count is wrong. ObjectNode uses this same writer, so the fix covers it too, and the tests include a class with no node of its own.
Fix: when the array holds exactly one element and not every extent is 1, the save throws
MatWriteError("... declaring dimensions [2,1] and holding 1 element, which no Dimension spells. Nothing was written."). The check is "every extent is 1" rather than "prod(dims) is 1", because [2,0.5] multiplies to 1. A mutant that uses the product is killed by the test.Tests (
dimsWriters.test.ts, one newitafter F1-21's): Parameter, Signal, AliasType and an unmodelled class, each pasted as an entry, as a cell element and as a struct field.<Element>.Changed existing test: none.
Left out (each needs its own call):
ObjectNode.serializeXml,_objectXml) write the mismatch.B-17: in a binary dictionary, a struct field an element lacks is written as [], in the struct's field order
Ruling: theme 2, "follow MATLAB, and measure it first", plus F1-16 (an absent field is
[]). The open follow-up (#132) said: "_structXml... is unmeasured in MATLAB".What was wrong: pasted into a binary dictionary at a Parameter's Value or an object property,
[{a:1},{}]was written<Element></Element>. StructNode's own path at an entry or a cell writesa = [].Root cause:
DataNode._structXmlwrote only the fields each element held, in that element's own key order.MATLAB truth (R2027a;
probe_struct_absent_fields_binary.m,struct_absent_fields_binary.truth.json): 144 dictionaries, made of 9 struct shapes plus 3 controls, each at 4 places.{}andnull.Fix: each element is written with the struct's field list, in order. A missing field is written as
[]. Any other fields follow, each name written once. An element that already matches is written byte for byte as before.Tests (new
structAbsentFieldBinary.test.ts, 14): these drive parseEntry, serializeSource and reopen against MATLAB's truth. 9 of the 14 failed before (wrong order, or a fieldless element). Mutants are killed: "[] after the held fields" by 8 tests, "no de-dup" by 1.Changed existing test: none.
Left out (need a decision):
B-18: open a bus or an enumeration whose element list is not a list, with a warning
Ruling: theme 1 (group F group 2), "warn and show what was read; never refuse the file, never crash", plus C-190. The open follow-up (#132) said: "an
_elementsthat is not an array still throws."What was wrong: an
_elementsof5,"x"or{a:1}threw_elements.forEach is not a function, and the whole open failed.Root cause:
BaseBusNode._parseElementsandEnumTypeNode.parseonly checked that_elementswas truthy.Deviation from the row:
nullis covered as well. It is the same hand edit. Before this change, null opened with no warning, and an untouched save rewrote it as[].Fix: the loop now runs only when
Array.isArray(_elements)is true. Any other present value gets onepart-unreadablewarning naming what was read (reportNotAListinnotAnElement.ts), shows no rows, and is kept as read (_keepNullElement).Tests (
nullBusElement.test.ts, new describe): 80 tests: Bus, ConnectionBus, ServiceBus and EnumTypeDefinition, each with 5, "x", {a:1} and null. Each case checks:68 of the 80 failed before: the truthy values threw, and null gave no warning and rewrote the list.
Changed existing test: none.
Left out: an
Elements_internalthat is itself a scalar still opens silently with no rows. It does not crash.B-22: Complexity and DimensionsMode are checked by either spelling of the property
Ruling: theme 2 and C-030 (Signal, BusElement and ValueType Complexity and DimensionsMode take only MATLAB's enumerals), "fix at the root".
What was wrong:
setProperty('complexity','bogus')was refused, butsetProperty('Complexity','bogus')was stored and saved. Setting the field spelling to the value already shown also marked the entry Modified.Root cause:
DataNode._propFormatchedkeyorcolumnbut nevernodeProperty. That skipped_rejectUnknownEnumeral,_isUnchangedCommitand SignalNode'sENUM_ATOMS.Deviation from the row:
DataModel.readPropertyValue, which captures the value an edit's undo needs, used the same lookup. With only the node fixed, undo of a field-spelling edit was refused, and the old value did not come back.Fix: one rule,
propNamed()inBaseNode.ts. It tries key or column first, then nodeProperty. Both_propForandreadPropertyValueuse it, and SignalNode checks the prop the name resolves to.Tests (new
enumFieldSpelling.test.ts, 48): both formats; MATLAB-written sig1, MyBus.x and MyValueType; both properties. The field spelling is refused with the key spelling's message and the save is unchanged. A legal value saves the same bytes as the key spelling. The shown value is no edit. Session undo restores the old value. 36 of the 48 failed before. The 12 that passed are controls.Changed existing test: none.
Left out: on a Parameter,
setProperty('Complexity', x)returns true and marks the entry Modified, but nothing is shown or saved.B-24: a one-element bus in a text dictionary is written back in the form MATLAB wrote it
Ruling: A4, "a no-op text save adds nothing that was not there ..., so a no-op save of the fixtures is byte-equal", plus D3, "Don't rewrite data nobody changed", plus theme 2 (MATLAB measured first).
What was wrong: a no-op save of the MATLAB-written
params.sldddiffered at byte 4731, where MyConnBus's one-elementElements_internalwas rewritten in array form.Root cause:
BaseBusNode._getSerializedPropertiesalways rebuiltElements_internalas{_array_class, _dimensions, _elements}.MATLAB truth (R2027a;
probe_bus_one_element_text.m,bus_one_element_text.truth.json, fixtures intest/fixtures/bus_one_element/):{_id, _object_class, _properties}._object_classon the elements.Fix: a bag read in the element form that still holds one element is written in that form. An added element gets
_object_classwhere MATLAB puts it. Binary output is unchanged. A one-element bus copied from a .mat now pastes into a text dictionary in the element form; MATLAB R2027a reads that form as the same Simulink.Bus (reviewer probe on generated /tmp files).Deviation from the row: when such a bus grows, the array elements drop the redundant
_object_class, as MATLAB's do (measured: MATLAB's own save of a grown bus), so the element read matches MATLAB's byte for byte.Tests:
sessionSerializeSource.test.ts: the untouchedparams.slddsaves as its bytes.busOneElementText.test.ts(11): the real session on MATLAB's file, and the truth.baseBusNode.test.ts: add, undo and redo, remove-first, ConnectionBus, the [1,1] array form kept, and remove-only._object_classby 3 tests, not inserting it by 3.Changed existing test: in
baseBusNode.test.ts, the pin "always emits the canonical _array_class form even when the source used _object_class" is replaced by "writes a one-element Elements_internal read as the element itself back in that form". A4 authorizes this, and the row says so. The pin's stated reason ("MATLAB would fail to load") applies only to_elementsunder_object_class, which is never written.Left out:
_ids on save. MATLAB reads core's ids back without complaint, so nothing was changed.hexobj_text.slddrespells10000000.0in a Dimensions value. That is not a bus, so it is outside this row.B-25: a Parameter's Dimensions and Complexity follow its Value after every edit
Ruling: A7, "a Parameter's Dimensions and Complexity rows are refreshed after an element removal collapses its Value. A plain bug." Theme 2 applies to the cases A7 did not measure (add, typed Value).
What was wrong: on
complex_binary.slddpRow, cutting 3 elements to 2, adding one, or typing[1 2 3 4]all still saved[1,3]complex.Root cause:
ParameterNode.childStructureChangedonly handled the collapse and restore cases, andsetProperty('Value')never updated the shape.MATLAB truth (R2027a;
probe_parameter_shape.m,parameter_shape.truth.json, 41 rows):'[1,N]'is kept, an empty Value keeps both, a struct Value keeps its Complexity, and text is[1 1]real.Fix:
_followValueShapeand_shapeFromValue(in ParameterNode, replacing A7's collapse and restore) run after every Add, removal, element edit, typed Value, and their undo and redo. They write only keys the file holds (locked down in review fix round 1, below)._restorerFor('Value')restores both keys on undo.MatlabVariableNode._setConstrainedValuenotifies the parent after an element edit. Only ParameterNode overrides that hook.Deviation from the row: MATLAB's symbolic save is not committed. It names the saving user and respells every entry, and m9MergedSaves would reject a new binary fixture. The test builds that file from
complex_binary.slddusing MATLAB's own tag, and asserts the tag against the truth.Tests (new
parameterShape.test.ts, 87):parameterShape.test.ts(at 34b5f46) against 34b5f46^ src fails 62 of 87, all on the shape as read; the 25 that pass are controls and the kept-shape exceptions (symbolic, empty, struct). (Run by the reviewer; the builder's copy for that run was refused by the permission system.)Changed existing test: none.
complexCollapse.test.ts(A7) passes unchanged.Left out (for triage):
1+0ito a real number, while core keeps it complex.setProperty('Dimensions'|'Complexity')on a Parameter returns true and changes nothing.Review fix round 1 (test only; src and dist unchanged)
parameterShape.test.tslock down that an edit writes no Dimensions or Complexity the file does not hold:complex_binary.slddwith pRow'sComplexity, or itsDimensions, taken out, through five of the probe's edits; texttypeLink.slddKp (no Dimensions) andtyped_mcos.slddpDoubleRow with its Complexity taken out. Droppinghas('Dimensions')fails 7 of the 14, droppinghas('Complexity')the other 7.parameterShape.test.tsnow has 101 tests; with B-25's src reverted, 71 of them fail.structAbsentFieldBinary.test.ts: a pasted_fieldsof['a', 5]or[null, 'a']saves as a struct with fieldaand reopens witha = 1. A member of_fieldsthat is not a string names no field and is written as none, as on main. Dropping the string check makes the save throw;String(field)writes<P Name="5" .../>; both fail all 4.parameterShape.test.ts: R2027a's own edit writes both keys; it is MATLAB-written dictionaries that can lack Complexity.slexpr('a+1')has Dimensions -1, and afterValue = [1 2 3]it has[1 3], in memory and aftersaveChangesin a dictionary. So replacing the -1 with the Value's size after a typed Value is MATLAB's own rule.Follow-ups seen, not built (for the maintainer's list)
_fieldsholds a non-string member shows a row for it (5 = [ ]) with no warning, and the binary save drops that row, as on main (theme 1: the open should warn).realwhatever its Value; with no Dimensions, the Dimensions row stays blank after an edit (the new tests check the save, not the rows).Gates at head 34b5f46 (base 38c20de; main is now 31b8ac3, #150, test only, and the PR merges cleanly)
rm -rf dist && npm run build: the tree stays clean.npm run verify: exit 0. 299 test files passed and 1 skipped. 10430 tests passed and 27 skipped. typecheck, smoke, check:pack, check:leak and check:browser all passed.DEX_READBACKS_CORPUS=... vitest run test/parity/readbacks.truth.test.ts test/parity/m9MergedSaves.truth.test.ts: 2 files, 45 tests passed.DEX_MATLAB_CMD="mw -using Bmain matlab", run on the head: structural.fidelity "ConnectionBus structural" and element.fidelity "ConnectionElement fidelity" (these reach B-24's element-form bus): 20 tests. In the combined run 19 passed and 1 failed: "edits element Name and round-trips" hit its fixed 60 s timeout while the load average was about 25. Run again alone, the ConnectionElement fidelity block passed 16 of 16.npm run verifyexit 0, 299 files passed and 1 skipped, 10448 tests passed and 27 skipped; opt-in readbacks and m9MergedSaves 45 of 45; dist unchanged.Implements the maintainer's rulings, as audited in group H (2026-10-19).