Skip to content

Damaged MAT input warns and shows what was read, instead of shifting values, overflowing the stack or opening as editable text - #126

Merged
ww-mw merged 6 commits into
mainfrom
damaged-mat-input-warns-and-shows-what-was-read
Oct 16, 2026
Merged

ww-mw merged 6 commits into
mainfrom
damaged-mat-input-warns-and-shows-what-was-read

Conversation

@ww-mw

@ww-mw ww-mw commented Oct 16, 2026

Copy link
Copy Markdown
Member

Implements the maintainer's 2026-10-16 group E ruling.

This PR covers six theme 1 items from the backlog, one commit each. The ruling for theme 1 (damaged input) says: "warn and show what was read; never refuse the file and never crash." The maintainer agreed each item's recommendation ("Agree your suggestions ... Add unit tests to lock down the fix."). Each recommendation is quoted below.

All six follow the same pattern. A damaged value is shown as a <RxC class, not decoded> row with no editor and no child rows, and the rest of the file reads as before:

  • A .mat file warns with part-unreadable, through the existing collectUndecoded / parsed.warnings path.
  • A model workspace does not warn, per the C-232 ruling.
  • A dictionary value shows the not-decoded row.

#122's warnings sink (recordPartialDecode) is for values that decoded in part, so no item here uses it, and no second sink is added. src/index.ts is unchanged. Every test was written first, and each one runs the real reader or session. Each commit includes its own rebuilt dist/, and npm run verify was green at each commit.

Gate

C-069: a struct array field slot that is not a matrix (c5850a7)

Recommendation: "(a): only damaged files reach it, it reuses the arm's existing damaged-struct refusal, and it keeps the change inside parseMatrix." Option (a) is: "Record the whole struct array as undecoded ('element k's field f is not a matrix'), as pastItsEnd does."

What was wrong: in a .mat struct array, if one element's field slot held a data element that is not a matrix, the slot was skipped and left no gap. That field's later values moved onto earlier elements, with no warning. Example: in a 1x3 array, element 3's a (5) was shown as element 2's.

Root cause: parseMatrix's struct-array field loop only adds a value when the slot is MI_MATRIX, then steps past the slot. So the field's list gets shorter, and structElementsHeld sizes the rows to the longest list.

Fix (MatParser.ts): a new branch in that loop. If the array has more than one element and the slot's tag is complete, the whole array is recorded as not decoded, with the reason element K's field F is not a matrix (a data element of type T). This matches the existing pastItsEnd branch. A 1x1 struct, and a tag cut short by the end of the bytes, are read as before. The node layer already builds the not-decoded row.

Tests:

  • matParser.test.ts "records a struct ARRAY with a field slot that is not a matrix as not decoded, and shifts no element onto another". It covers a 1x3 with a miDOUBLE slot (checking the exact variable, the exact warning, and that the next variable is unchanged), a miINT32 slot in a 2x2, and the nested paths t.f and c{2}.
    • Failed before: value was null, there was no undecoded, and the field lists were a:[1,5], b:[2,4,6].
  • matStructArray.test.ts runs the same case through createSession().addMatSource. The row is now <1x3 struct, not decoded> with 0 rows, and there is exactly one warning.
    • Failed before: the row was <1x3 struct> with 3 rows.
  • A cut control, "reads a struct array cut inside a field tag as one cut short", passes before and after.
  • Mutation checks: dropping the complete-tag guard fails only the cut control. Dropping the more-than-one-element guard fails only the existing "skips a field slot whose element is not a matrix".

Existing tests changed: none.

Left out: a 1x1 struct with a non-matrix slot still drops that field silently. Nothing can shift in that case, and the existing test pins it. This could be a follow-up. dex-core's G75 ports the old shift.

C-231: nesting with no depth limit (00e209f)

Recommendation: "(a), with a cap well under the measured ~1,200-level ceiling and checked against the node layer's own recursion depth." Option (a) is: "Thread a depth through parseMatrix. Past a cap, record the subtree as undecoded with a reason, which collectUndecoded already reports, and keep the rest of the file."

What was wrong: a value nested about 1,500 levels deep made the whole open fail with RangeError: Maximum call stack size exceeded. In a model workspace the section came out empty, and in a text dictionary the entry showed as <1x320033 char>.

Root cause: parseMatrix recurses into struct fields and cell elements with no limit, and the record walk does not catch the error.

Fix (MatParser.ts):

  • A new MAX_NESTING = 128. It is exported from the module only, not from the package. 128 is dex-core's cap.
  • parseMatrix(..., depth = 0). The struct-field and cell-element calls pass depth + 1.
  • At depth 128 or more, the reader reads the flags, dims and name, then records the value as not decoded with the reason nested more than 128 levels deep, so it was not read. The value's bytes are kept.
  • Every other caller starts at depth 0.
  • In mcosTypedNode.ts, the comment that said "a struct nested a few thousand deep is a legal file" is corrected.

Tests (new test/matNesting.test.ts, 9 cases; chains are built byte by byte): all 9 failed before, each for the reason given here.

  • The cap is 128. Before: undefined.
  • 5,000-deep cell and struct chains open. Level 129 is exactly the not-decoded value and holds the remaining levels' bytes. There is exactly one warning, and the variables around the chain equal a control file's. Before: RangeError.
  • Boundary: 128 levels are read in full with no warning. In a 129-level chain, only the leaf is recorded. Before: the leaf was read as double 7.
  • Through addMatSource, every row down to level 129 is present, and the last one is not decoded and not editable. toRow and serializeValue do not throw. Before: RangeError.
  • Dictionaries: decodeMatStream, a text dictionary (sparse_text.sldd entry c) and a binary one (sparse_binary.sldd entry c) open to the cap, and an untouched save writes the value back unchanged. Before: ok:false / <1x320033 char>.
  • Model workspace (cases.slx, synthetic workspace): both variables are shown, with no warning. Before: the section was empty.
  • Mutation checks: an off-by-one cap is caught by 8 cases, dropping the cell depth + 1 by 5, and dropping the struct one by 4.

Existing tests changed: none.

Left out:

  • The top row of a 5,000-deep cell shows 128 pairs of braces, which is the node layer's existing display for nested cells.
  • A capped cell element copies as its placeholder text, as every not-decoded cell element already does.
  • dex-core's G78 reads 129 levels and shows class unknown. It should follow this reader.

C-068: a numeric value whose bytes end before its data (7c9b670)

Recommendation: "(b), ruled together with the agenda's 'Short values in the node layer' bullet." Option (b) is: "The node layer builds a not-decoded node (_createUndecoded with a reason such as 'its payload is missing') whenever a numeric variable declares elements and has value null, real or complex."

What was wrong: a numeric variable (real, complex, logical or integer) that declares elements but whose bytes end before its data was shown as null / [null], editable, with no warning. Before #116, the complex case threw and lost the whole file. #116 stopped the throw by building the value as a real one.

Root cause: the parser keeps value: null with no undecoded mark, and its tests pin that. Neither _createFromMatNumeric nor collectUndecoded handled this case.

Fix:

  • MatlabVariableNode._createFromMatNumeric builds the not-decoded node when the value is null, more than 0 elements are declared, and the class is not unknown. A model whose workspace part is cut short opens even when the cut lands in a complex variable #116's guard is kept for 0 elements or negative dims.
  • MatParser: a new holdsNoData check gives "P" declares N element(s) and holds no data, so its value was not read. A variable already named by a "read short" warning is skipped, so a cut record warns once.
  • The parser's value: null is unchanged.

Tests (new test/matNoPayload.test.ts, 7 cases):

  • Real 1x1 and 2x3, complex 1x1 and 1x2, logical, and int16 3x2 variables, each with no data, show <RxC cls, not decoded>, read-only with 0 rows. A 0x0 variable still shows [ ]. There are exactly 6 warnings.
  • Nested s.a and c{1} cases.
  • A cut plain or compressed record warns once.
  • A parser control, which passes before and after.
  • A synthetic workspace, with no warnings.
  • complex_ws.slx z at all 31 lengths of A model whose workspace part is cut short opens even when the cut lands in a complex variable #116's window.
  • A text dictionary's cdata and a binary dictionary's hex. An untouched save writes them back unchanged.

Before the fix, 6 of the 7 failed on the row (null, [null], editable). With only the node half applied, cases 1 and 2 still failed because warnings was undefined, so the warning half fails for its own reason too. Mutation checks: dropping the dedupe fails only case 3. >= 0 in either bound fails case 1. Dropping the unknown exclusion fails exactly the two dictionary tests that pin a damaged class-unknown stream, so the exclusion stays.

Existing tests changed: none.

Left out:

  • A cell element copies as its placeholder text. A top-level variable or struct field copies as the bytes it was read from, where before it copied as null.
  • The legacy .slx modelworkspace.mat path passes on parseMat's warnings, so there this value warns. That path already warns for every other undecoded reason.
  • dex-core's G81 should follow.

C-014: a text dictionary's MAT stream that does not decode (f8719f2)

Recommendation: "(c). It mirrors the existing reportUndecoded harvest and needs no change to the NodeClassType interface. Use a dedicated flag rather than _undecoded, so the char node's write-back is unchanged."

What was wrong: when a text dictionary's cdata failed to decode, it was shown as an ordinary char holding the encoded text. It was editable and raised no warning.

Root cause: a silent catch (_e) in MatlabVariableNode.parseCdata. NodeClassType.parse has no warnings channel.

Fix:

  • There is still no interface change, and _undecoded is not used.
  • The harvest reuses the per-entry decode-failure channel that the binary hex path already uses (recordDecodeFailure / takeDecodeFailures, drained by SlddNode.parse for each entry). Option (c)'s "post-build walk" would have been a second harvest in the same loop. Both arms of parseCdata now record the reason with shownAs = 'its encoded text', then build the same char as before.
  • The char is marked _readFromStream = true, which is the dedicated flag, so it is display-only under the C-012 ruling.
  • The write-back is unchanged, because _rawInput is replayed.
  • MatlabVariable.md documents this.

Message: "c" holds a value stored encoded that could not be read, because its MAT stream did not decode (...); it is shown as its encoded text and saved unchanged.

Tests (new test/cdataDecodeFailure.test.ts, 10 cases):

  • A damaged flags word in sparse_text.sldd c{1} gives exactly one warning, and every other entry is as in the intact file.
  • c{1} is display-only and setProperty('Value') refuses it, while c{2} stays editable.
  • An untouched save is byte-identical.
  • A rename keeps the cdata and warns under the new name.
  • A damaged whole-entry stream (nd_text.sldd A) warns and is display-only.
  • Two damaged preambles (endian mark, reserved word) are covered.
  • Guards: the binary hex message is unchanged, a failed paste is not reported against the next open, and all 23 intact text fixtures (99 streams) raise no warning.

Before the fix, 6 failed (warnings undefined, valueEditable true) and the 4 guards passed. Mutation checks: no record call fails 5 tests, no display-only flag fails 4, and hard-coding "not decoded" fails 5.

Existing tests changed: none.

Left out:

  • The edit refusal still says the entry "is stored as a MAT-file stream", which is still true. The warning is what says the stream could not be read.
  • Two cases pin V8's TypeError text.

The class-255 workspace variable: a flags word that is overwritten (7091bba)

Recommendation (agenda, group C): "(a) catch it per variable and leave that variable out, as #114 does for a struct array with no element read (the reviewer's suggestion: catch in addWorkspaceEntry) ... Recommendation: (a), which matches the ruling's 'no refusal'."

What was wrong: a variable whose flags word was overwritten (for example with 0xFFFFFFFF over complex_ws.slx's z) threw TypeError: Cannot read properties of null (reading 're'). That refused the whole model, or the whole .mat.

Root cause: the class is unknown with the complex bit set. That reaches _createFromMatNumeric's complex branch with a null value, and C-068's branch deliberately excludes unknown.

Fix:

  • There is a per-variable catch in ModelSectionNode.addWorkspaceEntry (no warning, per C-232) and in MatNode.fromParsed.
  • MatNode.fromParsed warns "z" could not be read (<reason>), so it is shown as not decoded. It skips that warning when an existing warning already names the variable or a part of it (z, z., z(, z{).
  • A new MatlabVariableNode.createNotDecoded builds the row, and C-068's branch now uses it.

One departure from the recommendation: the caught variable is not left out. It is shown as a <1x1 unknown, not decoded> row that keeps its bytes. That follows the theme 1 ruling's "show what was read", and it matches how a dictionary already shows a damaged class-unknown stream.

Tests (new test/unmodelledClassVariable.test.ts, 18 cases):

  • Four workspace variables (complex_ws.slx z, cases.slx cplxScalar and cplxVec, sparse_ws.slx dComplex), each with its parsed flags word (asserted to be class 6, complex) set to 3 values. Every other row and section equals the intact file's, with no warnings.
  • The same damage nested in a cell and in a struct.
  • Three .mat files, where the only warning is the reader's own.
  • Nested .mat cases, which give exactly one warning per element.
  • A host parse that keeps only a zz warning, which catches a bare prefix match.

Before the fix, all 18 failed on the TypeError and none on an assertion. Mutation checks: an exact-name-only match fails only case 17, a bare prefix match only case 18, no dedupe cases 14-17, and no fallback warning only case 18.

Existing tests changed: none. The two dictionary tests that pin a damaged class-unknown stream pass unchanged.

Left out:

  • A real (not complex) value of an unmodelled class still opens as an editable null, class unknown. That is a display question, not a refusal.
  • Damage nested inside a variable turns the whole top-level variable not decoded (the cell c, not only c{1}), as in the binary dictionary.

C-210: a System Composer definition with no type (f4150a9)

Recommendation: "(b), at low priority. A present-but-typeless record is damage, not absence, and the test's own comment calls the result 'a wrong answer that looks exactly like a right one'." Option (b) is: "Warn part-unreadable, naming the definition, from both the text and the binary catalog reader."

What was wrong: if a catalog definition lost its type, a StructType was shown as a Data Interface (its Simulink class), with no warning.

Root cause: both readers recorded a missing type as '' and stayed silent: SlddNode._parseSystemComposer for text, and ScCatalog.scanScXml for binary.

Fix:

  • A new ScCatalog.untypedDefinitions(catalog).
  • SlddNode.parse adds one part-unreadable per untyped definition, at the one place both formats pass through: The System Composer definition "X" has no type, so its Kind is taken from its Simulink class and may be wrong (a struct type reads as a Data Interface).
  • The text reader also records a non-string type as ''.
  • Unchanged: Kind still comes from the class. An absent or empty catalog stays silent, as measured against MATLAB. A type string that is present but not recognised stays silent.

Tests:

  • slddNode.test.ts, new: a numeric type is recorded as '' with one warning. Controls: a FutureType string gives no warning, and pristine arch.sldd gives none.
  • scCatalog.test.ts, new:
    • The type is cut from arch_binary.sldd's tag, which gives exactly one warning for StructType.
    • The same cut in its text twin arch_binary_as_text.sldd gives an identical warning list, catalog and Kinds.
    • Both intact files give no warning.
  • Before the fix, 4 failed on an assertion and the 3 controls passed. Mutation checks: text-only, binary-only, modeled-types-only, no string check, and warning on an unrecognised type each fail their own tests.

Existing test changed: slddNode.test.ts "degrades a definition whose record lost its type to the entry's raw Simulink class". Its expect(warnings).toEqual([]) now expects two part-unreadable warnings, naming DataInterface and StructType. The test pinned exactly the silence this ruling removes; its own comment said "pinned rather than fixed". Its Kind and '' assertions are kept, and its comment and the file header are reworded.

Left out: the corpus was not re-measured. The scope found 0 untyped definitions out of 29,200 at 8e33408, and string types are read as before. vscode needs no change, since it shows warnings in the generic banner.

ww-mw added 6 commits October 16, 2026 17:28
… decoded, not shifted

MATLAB writes every field of every element of a struct array as a matrix, so
a slot that is anything else only comes from a damaged file. The reader
stepped over such a slot and left no hole in that field's list, so the
elements after it moved up one in that field and not in the others: in a 1x3
whose element 2's a is not a matrix, element 3's a showed as element 2's, and
the file said only that two elements were read.

Such an array is now recorded as not decoded, as one whose field names lie
past its end already is: it shows as <1x3 struct, not decoded>, with no rows
and no editor, and a .mat says why ("s" was not decoded: element 2's field a
is not a matrix (a data element of type 9).). The file still opens, and every
other variable in it reads as before.

Two cases keep today's reading. A field tag cut by the end of the bytes is a
cut, so the elements before it are kept and the file says how many of the
declared elements could be read. A 1x1 struct with such a slot only leaves
that field out, since nothing can shift there; that is still pinned by "skips
a field slot whose element is not a matrix".

dist is rebuilt from src.
… not a refused file

The MAT reader recursed into every struct field and cell element with no
bound but the call stack. A 1x1 cell nested about 1,600 deep (a 77 KB file)
threw "Maximum call stack size exceeded" out of parseMat, so the whole .mat
was refused, the variables beside it included. A dictionary value or a model
workspace holding such a value failed to decode as a whole: a text
dictionary showed the entry as its cdata text (<1x320033 char>), and the
model's Model Workspace section came up empty. Where the stack ran out moved
with the engine and the stack size.

The reader now reads 128 levels (MAX_NESTING, the number dex-core uses). The
value at level 129 is read as far as its class, size and name, and is
recorded as not decoded: "nested more than 128 levels deep, so it was not
read". It keeps the bytes it was read from, as every value the reader does
not decode does, and nothing under it is read. The rest of the file reads as
before. A .mat says which value it stopped at ("deep{1}...{1}" was not
decoded: ...). A model workspace says nothing, per the C-232 ruling. An
untouched dictionary save writes the value back byte for byte.

The deepest variable in the corpus is 6 levels, so no real file reads
differently. In a model workspace the workspace struct is itself level 1, so
a variable there is read to its own level 127.

dist is rebuilt from src.
…ecoded, and a .mat says why

A numeric or logical MAT value whose bytes end after its name, before its real
part's tag, declares elements and holds none. MATLAB writes no such value, so
only a damaged file holds one. The reader leaves its value null, which means no
payload was there at all. The node layer took that null for the value: a 1x1
showed null, a 2x3 [null], a 1x1 logical false, and each offered an editor for a
value nothing read. A complex one did the same, since #116 built it as the real
one cut at the same point. Nothing said so.

The maintainer's ruling of 2026-10-16 (group E, theme 1: warn and show what was
read) makes it what every value the reader did not decode already is. It is one
row, <2x3 double, not decoded>, with no editor and no element rows: its class
(logical for a logical) and its size are what was read. This holds in a .mat, in
a struct field or a cell element, in a model workspace, and in a text or binary
dictionary value. A .mat file now says why: "x" declares 6 element(s) and holds
no data, so its value was not read. A model workspace stays silent, as the C-232
ruling says. When the record holding the value is the one the file cuts short,
the record's own "read short" warning already names it, so that one loss is
reported once.

Change: MatlabVariableNode._createFromMatNumeric builds the not-decoded node for
a null value that declares at least one element, of any class the reader models.
It spells the placeholder with MatParser's undecodedValue, now exported from the
module (not from the barrel). MatParser.collectUndecoded adds the warning, and
parseMat passes it the variables a record warning already names. The parser's
own reading (value null) is unchanged, as matParser.test.ts pins it. A class the
reader does not model ('unknown') is left as it was. That is the CLASS-255 item,
and lifting the exclusion fails the two dictionary tests that pin it.
An empty value (0x0) with no payload is still [ ].

Test: test/matNoPayload.test.ts, 7 cases, written first. Before the fix, 6
failed. The rows showed null, [null] or false, editable, in a .mat, a struct
field, a cell element, a cut record, a synthetic model workspace, #116's 31 cut
windows of complex_ws.slx's z, and a text and a binary dictionary value. The
7th, the parser's null, passes before and after. With the node change alone, the
2 warning cases failed with no warning. Mutants of the node arm's and the
warning's > 0 bound, and dropping the dedupe, are each caught. No existing test
changed.

dist is rebuilt from src.
…xt is display-only

C-014, under the maintainer's group E ruling of 2026-10-16 on damaged input: warn and
show what was read, never refuse the file. parseCdata already showed a cdata value whose
stream does not decode as the char of its six-bit text, and an untouched save already
wrote the text back. But it said nothing, where a binary dictionary's hex of the same
damaged bytes warns, and the char took edits, where every other value a text dictionary
holds as a MAT stream is display-only (the C-012 ruling of 2026-10-15). An edit would have
saved what was typed in place of the stream.

parseCdata's two failure arms (a stream decodeMatStream refuses, and a throw from the
node builders) now go through _createUndecodableCdata. It records the failure through the
per-entry harvest a hex value's failure already uses (EncodedValue.recordDecodeFailure,
which SlddNode.parse collects around each entry), so no second warnings channel is added.
It also marks the char _readFromStream, so the char is display-only. Its reason is the
clause the hex path uses: decodeMatStream's reason, or "its MAT stream did not decode
(...)". recordDecodeFailure now also takes how the value is shown. The default is "not
decoded", the hex placeholder's wording, so the hex message is unchanged. The cdata
passes "its encoded text":

  "c" holds a value stored encoded that could not be read, because its MAT stream did
  not decode (...); it is shown as its encoded text and saved unchanged.

The write-back is unchanged: _rawInput is still replayed, and an untouched save is byte
for byte the file read.

New test/cdataDecodeFailure.test.ts, 10 cases, built from MATLAB's own streams in
sparse_text.sldd and nd_text.sldd:
- An overwritten flags word in a cell element or a whole entry gives one part-unreadable
  warning for that entry. The value is the char of its text, display-only, and its entry
  can still be renamed. The JSON element beside it stays editable, and the untouched save
  is byte-identical.
- matStreamSites' two refused preambles, endian mark and reserved word, warn with that
  reason when the dictionary is opened.
- Guards: a hex value's message is unchanged. A damaged stream parsed outside an open is
  not reported against the next open. None of the 23 intact text dictionaries in
  test/fixtures (99 streams) warns.
Six of these failed before the change, because there was no warning and the value took
edits. No existing test changed.

dist rebuilt (rm -rf dist && npm run build).
…d, and its file opens

A model-workspace or .mat variable whose array-flags word is overwritten names
a class the reader does not model (255, say, or 18), so MatParser reads it as
class 'unknown' with no value. The flags byte can still say complex, and then
the node builders read .re of its null value. The throw refused the whole file:
a model whose workspace part held one variable like that (0xFFFFFFFF over
complex_ws.slx's z, for example), and a .mat holding one, at the top or inside
a cell or a struct. #116 left this open, since it is damage and not a cut.

The maintainer's group E ruling of 2026-10-16 puts this item under theme 1:
warn and show what was read, never refuse the file and never crash. It takes
the item's recommendation and catches the failure per variable. The variable is
shown as what was read of it: its name, its class and its size, as one row,
<1x2 unknown, not decoded>, with no editor and no rows. A binary dictionary
already shows the same bytes that way. The rest of the file opens as it would
have.

ModelSectionNode.addWorkspaceEntry catches a builder throw and adds the
not-decoded row. It does not warn: the C-232 ruling says a model workspace does
not report a value it did not decode. MatNode.fromParsed does the same for each
variable of a .mat. A .mat says why once. MatParser already warns about a value
of a class it does not model, wherever it stands ("z", "c{1}", "s.a"), so a
variable that warning names, or names a piece of, gets no second warning. A
variable nothing names, for example in a host's own parse handed to
addMatSourceParsed, gets one: "z" could not be read (...), so it is shown as not
decoded. MatlabVariableNode.createNotDecoded builds the row. The no-data arm
that C-068 added now uses it too, with no change in behaviour.

Left as it was: _createFromMatNumeric's 'unknown' exclusion. Lifting it fails
the two dictionary tests that pin a damaged stream as not decoded, with a
warning. Also unchanged: a real (not complex) value of an unmodelled class still
opens as null, editable, class unknown. That is a display question, not a
refusal.

Test: test/unmodelledClassVariable.test.ts, 18 cases, written first. Before the
fix, all 18 failed, each on the TypeError "Cannot read properties of null
(reading 're')". The cases are:
- complex_ws.slx z, cases.slx cplxScalar and cplxVec, and sparse_ws.slx
  dComplex, each with its flags word (found by parsing) set to 0xFFFFFFFF,
  0x000008FF and 0x00000812. Each is a not-decoded row. Every other workspace
  row and every other section equals the intact file's, and there are no
  warnings.
- A synthetic workspace with the damaged value inside a cell and a struct.
- A .mat with each of the three words. The only warning is the reader's own.
- A .mat with the damaged value nested in a cell and a struct. The only
  warnings are the c{1} and s.a ones.
- A host's parse with no warning for the variable. It gets one.
Mutants of the dedupe are each caught: an exact-name match only, a bare prefix
match, no dedupe, and no fallback warning. No existing test changed.

dist is rebuilt from src.
…ictionary formats

C-210, under the maintainer's group E ruling of 2026-10-16 on damaged input: warn and
show what was read, never refuse the file. A dictionary's System Composer catalog lists
each definition with the type that models it, and that type is the only thing that
tells a struct type from a data interface (both are a Simulink.Bus). A definition whose
record lost its type was recorded as the empty string and its entry took its Kind from
its class, so a struct type read as a Data Interface. Nothing said so, in either the
text or the binary format. test/slddNode.test.ts pinned that silence.

What was read is still shown: the definition is still listed with an empty type, and
the Kind is still the class's. What is new is one part-unreadable warning for each such
definition, naming it: The System Composer definition "StructType" has no type, so its
Kind is taken from its Simulink class and may be wrong (a struct type reads as a Data
Interface).

The warning is raised in one place, SlddNode.parse, right after the catalog is read, and
it covers both formats. The binary reader hands its catalog over on __scCatalog, and the
text reader's comes from _parseSystemComposer. So the same damage says the same thing
however the dictionary was saved. ScCatalog.untypedDefinitions lists the names: own keys
of either bag whose type is empty or not a string, interfaces first, each once. The text
reader now records a type that is not a string (a number, say) as the empty string, as
it records a missing one. Before, it kept the number in the catalog as it was.

Unchanged: an absent or empty catalog stays silent (the MATLAB-measured note in
ScCatalog.ts), a nameless record is still skipped, and a type that is there but not one
the reader maps stays silent too, since it may be a newer release's. No real dictionary
is expected to warn: the scoping measured 0 typeless definitions of 29,200, in the 10
catalog-bearing dictionaries among 225 .sldd files.

Tests, written first:
- test/slddNode.test.ts: the existing test for a lost type now expects two
  part-unreadable warnings, for DataInterface and StructType, in place of none. That is
  the one existing assertion changed, because it pinned exactly the silence the ruling
  removes. Its Kind and empty-type assertions are kept. New: a type that is a number is
  recorded as '' and warns once. Controls: an unmapped type string, and the pristine
  arch.sldd, give no warnings.
- test/scCatalog.test.ts: arch_binary.sldd with the StructDataType type attribute cut
  from StructType's definition tag (the cut is asserted to land once) warns once, part
  StructType. StructType's Kind is Data Interface and every other Kind is as in the
  intact file. The same cut in its text twin, arch_binary_as_text.sldd, gives an equal
  warning list, an equal catalog and equal Kinds. Control: both intact forms give no
  warnings.

Before the change, the 4 new or changed warning tests failed on their assertions (0
warnings, and 42 kept in the catalog), and the 3 controls passed. Mutants: dropping the
text reader's string check, scanning only modeledDataTypes, warning for one format only
(either way round), and warning on any unmapped type each fail at least one of them.

dist rebuilt from clean (rm -rf dist && npm run build).
@ww-mw
ww-mw merged commit 5cd013c into main Oct 16, 2026
4 checks passed
@ww-mw
ww-mw deleted the damaged-mat-input-warns-and-shows-what-was-read branch October 16, 2026 23:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant