Derive a struct's fields from its elements when MATLAB writes no _fields - #40
Merged
Merged
Conversation
Current MATLAB no longer writes `_fields` into a text dictionary: build
27.1.0.3353139 wrote it, 27.1.0.3393633 does not, and the field names live
only as the keys of each `_elements` entry. `StructNode.parse` built its
children from `_fields` alone, so every struct in a current-build text .sldd
showed no field rows at all -- and because `serializeElement` iterates the
same list, editing anything under the entry re-serialized the struct as
`_elements: [{}]`, deleting its fields from the saved file.
Four parsers build this envelope and three already derived the list for
themselves (`BinarySlddParser` from `Object.keys(parsed[0])`, `McosParser`,
`MatlabVariableNode`), which is why only the text channel -- the one that
passed MATLAB's JSON straight through and trusted the key -- broke. Move the
decision into one `fieldsOf` helper at the point all four funnel through: an
explicit `_fields` still wins, since it is the only statement of field ORDER
an envelope carries, and absent it the names are the keys of element 0 --
element 0 and not a union, because a struct array shares one field list by
definition and that is the rule the binary parser already applied.
`serial._fields` is therefore always an array, so `serializeElement`,
`_renameField` and the add/remove/restore hooks read one list that is already
right instead of each deciding what a missing key means; `addChildNode`'s
`if (!this.serial._fields)` branch was a latent second bug on the same line
(it would have built a list holding only the new field) and is now
unreachable. The writer stays faithful both ways: `_fieldsDeclared` records
whether the input declared the key and only then is it emitted, so a save
does not add `_fields` back to a file MATLAB left it out of, while a struct
we invent ourselves still declares an empty one.
Gated by test/structFieldsAcrossBuilds.test.ts, which asserts both builds'
bytes as the same assertion, walks the text channel against the binary one
to full depth, and checks both halves of the write. The two cross-channel
carve-outs this defect had forced in binaryWriteBackGate.test.ts are gone
with it -- the walk now stops nowhere.
Harden two enumerations in the same pass, both inert against every byte
MATLAB has been measured writing:
- `isNumericClass` becomes the pattern `u?int(8|16|32|64)` and
`parseTypedValue` stops keeping a second copy of the same list in its
`case` labels. Defect 27 was precisely that duplication: int64/uint64
missing from both copies, so the value fell through to the bare-text
return and the writer spelled it `Class="char"`. A pattern cannot omit a
width.
- `IsComplex` accepts `"true"` as well as `"1"`, through one shared helper
rather than three literal comparisons -- the latitude `parseTypedValue`'s
`logical` arm has always taken. An unrecognized spelling would not merely
mis-display: it drops the imaginary parts from the value and from the next
save.
`half` is deliberately left out of the numeric pattern. Measured: the entry
site accepts any class and formats it as an integer, so `Class="half">1.5`
writes back `2` and a non-numeric body writes back `0`, while the field
site's fallback loses the class but keeps the characters. Admitting a class
the save path cannot format trades a wrong type for wrong digits, so the
narrower failure stays.
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.
The defect
Current MATLAB no longer writes
_fieldsinto a text dictionary. Build27.1.0.3353139wrote it;27.1.0.3393633does not, and the field names live only as the keys of each_elementsentry.StructNode.parsebuilt its children from_fieldsalone, so every struct in a current-build text.slddshowed no field rows at all — and becauseserializeElementiterates that same list, editing anything under the entry re-serialized the struct as_elements: [{}], deleting its fields from the saved file. No edit to the fields themselves was needed: any edit under the entry drops_rawInputand the re-serialize does the rest.Measured on MATLAB's own bytes: all 8 structs in
cellarr_text.slddandcellstr_text.slddcame up empty (including struct-array elements and a struct inside a cell), while every struct in the oldertyped_text.sldd,nd_nested.slddandobject_props_text.slddwas correct.Why only one of four channels broke
Four parsers build this envelope, and three already derived the list for themselves —
BinarySlddParser(_fields: Object.keys(parsed[0])),McosParser,MatlabVariableNode. Only the text channel, the one that passed MATLAB's JSON straight through and trusted the key, was wrong. The channel that derived from structure survived the format change; the channel that trusted the spelling did not.The fix
One
fieldsOfhelper at the point all four channels funnel through:_fieldsstill wins — it is the only statement of field order an envelope carries;serial._fieldsis therefore always an array, soserializeElement,_renameFieldand the add/remove/restore hooks read one list that is already right rather than each re-deciding what a missing key means.addChildNode'sif (!this.serial._fields) { = [] }was a latent second bug on the same line — it would have built a list holding only the newly added field — and is now unreachable.The writer stays faithful in both directions:
_fieldsDeclaredrecords whether the input declared the key, and only then is it emitted, so a save does not add_fieldsback to a file MATLAB left it out of. A struct we invent (createDefault) still declares an empty one — the emit policy exists to avoid contradicting a MATLAB file, and there is none in that case.Gate
test/structFieldsAcrossBuilds.test.ts(17 tests) asserts both builds' bytes as the same assertion — a reader is not allowed to care which build it is reading — walks the text channel against the binary one to full depth, and checks both halves of the write: every field kept,_fieldsnot invented,_fieldspreserved in MATLAB's order where it existed. Reverting the derivation alone fails 11 tests.The two cross-channel carve-outs this defect had forced in
binaryWriteBackGate.test.tsare gone with it: the full-depth walk now stops nowhere (32 → 38 rows compared) andsStr's field is compared like everything else.Two enumerations hardened in the same pass
Both provably inert against every byte MATLAB has been measured writing, and both closing a failure mode that already happened once:
isNumericClassis a pattern, not a list, andparseTypedValueno longer keeps a second copy of the same set in itscaselabels. Defect 27 was precisely that duplication —int64/uint64missing from both copies, the value falling through to the bare-text return, the writer then spelling itClass="char".u?int(8|16|32|64)cannot omit a width.IsComplexaccepts"true"as well as"1", through one shared helper rather than three literal comparisons — the latitudeparseTypedValue'slogicalarm has always taken. An unrecognized spelling would not merely mis-display; it drops the imaginary parts from the value and from the next save, which is defect 53's exact shape.halfis deliberately not in the numeric pattern. Measured: the entry site accepts any class and formats it as an integer, soClass="half">1.5writes back2and a non-numeric body writes back0, while the field site's fallback loses the class but keeps the characters. Admitting a class the save path cannot format trades a wrong type for wrong digits, so the narrower failure stays — and the field site was deliberately not aligned with the permissive entry site, which was the change this audit set out to make until the measurement said otherwise.Verification
npm run verifyclean: typecheck, build, smoke, 4907 tests passed / 26 skipped / 166 files, pack (dist-only), leak, browser-safe.