Repository navigation
An MCOS object's numeric values keep their MATLAB class, and a .mat object array keeps each element's class - #128
Merged
Merged
Conversation
…ows MATLAB's digits
C-033, per the maintainer's group E ruling of 2026-10-16 (theme 2: follow MATLAB, and
measure it first). A Simulink.Parameter whose Value is int32([100 200 300 400]) showed
class double in a .mat and a model workspace, where both dictionaries show int32; a
logical row showed double; and single(3.3) showed 3.299999952316284 where both
dictionaries show 3.3. The corpus twins param_* and parity_* show all of these.
The class. McosParser.resolveValue handed every numeric scalar and row over bare, and
stamped double on every column and matrix. The node layer reads a bare number as a
double. A real value of any class other than double is now the binary dictionary
reader's typed spelling of it: {_type: 'int8', _value: '-42'}, '[100, 200, 300, 400]',
'Matrix(3,1)...'. A logical row is the typed logical literal. A double is unchanged.
Two readers of resolved values follow:
- the function-handle marker is read off the raw cell, since the uint32 word now resolves
typed;
- an int32 enumeration's `int32.Data` is read from its typed spelling, so each
enumeral keeps its number.
The digits. MatParser read a single as the double it widens to. A value of class single
now reads each element as MATLAB spells that float32. This was measured on R2027a, in
both dictionary formats, over 4,086 float32 bit patterns and 7 exact ties; the fixture
keeps 4,082 of the patterns and 5 of the ties. The rule is C's
%.6g where that gives the same single, otherwise %.9g with ties to even, and %.10g for
realmax('single'). It is not "the shortest decimal that is the same single": MATLAB
writes single(1/3) as 0.333333343. The shortest-decimal rule differs from MATLAB on
3,520 of the 4,082 patterns. This rule matches all of them, and all 127,743 non-zero
singles in a customer text dictionary in the corpus. It is lossless, because the
number is the same float32. An exact-arithmetic path handles the common range; it
gives the same numbers on every 31st bit pattern, and a million singles take 75 ms,
not 400 ms. A double stored as miSINGLE keeps its double.
Corpus, all 168 .mat/.slx files, main against this: 82 rows change class (param_* int32
and logical, topMat.mat's uint64 mID). 88 rows change digits (allMidiSeq.mat 74,
parity_* 10, param_* 4). Nothing else changes, and no file newly fails to load.
New fixtures, made by MATLAB (test/fixtures/mcos/make_typed_fixtures.m):
typed_objects.mat, typed_ws.slx, typed_mcos.sldd and typed_mcos_binary.sldd, with a
truth file. typedMcosFixtures.test.ts checks four things:
- every value presents alike in all four venues, with MATLAB's class and size;
- the .mat's numbers are MATLAB's, bit for bit;
- the sweep shows MATLAB's own text, element by element;
- a Parameter copied out of the .mat into either dictionary reopens as MATLAB's own
entry. In the binary dictionary the Value tag is also MATLAB's own.
matSingleDigits.test.ts locks the digits on hand-built MAT bytes: plain, complex,
sparse, through decodeMatStream, ties, and a lossless sweep.
Existing tests changed, each because it pinned exactly what the ruling removes:
- mcosColumnVector.test.ts, "control: a logical 1x6 is still the bare row of booleans
it was": now the typed logical row, shown as class logical.
- mcosParser.test.ts, "array of logicals resolves as boolean[]...": a logical [0 0]
Dimensions is now {_type: 'logical', _value: '[0, 0]'}.
- complexMcosValue.test.ts: a comment only. The case still tests the formatter on a
widened double, which a .mat no longer hands over.
DESIGN.md: two known limitations are removed, because this commit fixes them: "A REAL
array inside an MCOS object is classed double" and "A single shows the double it
widens to".
Not done: an empty array of a class other than double still resolves to [], which
reads as a double 0x0. The binary reader's classed empty carries BINARY_EMPTY_KEY,
whose meaning for a value out of a .mat is a separate question.
…, as a dictionary does
F1-47, per the maintainer's group E ruling of 2026-10-16 (theme 2: follow MATLAB, and
measure it first; recommendation (a)). An object array held by an MCOS object's
property, in a .mat or a model workspace, was named after its first element, and no
element kept a class of its own. A config set's Components read as a Simulink.SolverCC
array, and a copy of the config set pasted into a dictionary wrote all nine components
as Simulink.SolverCC. A dexdata.DerivedSignal beside a Simulink.Signal read and showed
as two DerivedSignals.
The binary dictionary reader already reads such an array as MATLAB's text format names
it (the maintainer's D6 and D15), but its rules were private to BinarySlddParser. They
move to one new module, src/datamodel/parser/objectArrayClass.ts: arrayClassOf,
heterogeneousBaseOf, componentsBaseOf and the lists they use, unchanged but for the
wording of their comments. BinarySlddParser imports them and behaves as before
(objectArrayElementClass.test.ts is unchanged and green). McosParser.resolveValue now
uses the same rules:
- the array is named by componentsBaseOf where its place names a class (a ConfigSet's or
an RTWCC's Components), and otherwise by arrayClassOf over its elements' classes;
- each element of another class than the array's carries `_object_class`, ahead of its
`_properties`, as the binary reader writes it;
- a later element whose object names no class row is one of the first element's class,
as a classless later element is in a binary dictionary.
buildProperties passes the owning object's class and the property's name. A struct field
and a cell element pass neither, as in the binary reader.
Measured on R2027a: MATLAB's class() of each array, read back out of the new fixture, is
the class the rule names. It is Simulink.ConfigComponent for both config sets' Components
and for their RTWCC's Components, and Simulink.Signal for a dexdata.DerivedSignal beside a
Simulink.Signal.
Corpus, all 168 .mat/.slx files, this commit's parent against this commit:
- Of the decoded MCOS variables, exactly two change: the config set cs in
findVar/dConfig.mat and in models/topMat.mat. Their Components and RTWCC Components are
now Simulink.ConfigComponent arrays, with each component's own class.
- The node tree walk is unchanged, 0 of 419,476 rows, because a ConfigSet shows no
Components rows. No file newly fails to load.
New fixture, made by MATLAB (test/fixtures/mcos/make_configset_fixture.m):
configset.mat, 23 KB, and configset.truth.json. It holds cs (grt.tlc), csErt (ert.tlc,
so its RTWCC's Components holds an ERTTargetCC) and mixed, an NdHolder whose Kids is
[dexdata.DerivedSignal, Simulink.Signal].
New tests. On the parent commit, objectArrayClass.test.ts does not load, because the
module does not exist, and 10 of the 14 tests in mcosMixedObjectArray.test.ts fail, each
on the class the old reader gave. The other 4 are preconditions and controls.
- objectArrayClass.test.ts: the rule itself, the binary reader's assertions asked of the
module.
- mcosMixedObjectArray.test.ts:
- configset.mat against MATLAB's truth: each array's class, and each element's in order;
- the node layer: mixed's Kids shows <1x2 Simulink.Signal>, an ObjectNode and a
SignalNode;
- each value copied out of the .mat and pasted into a binary dictionary writes each
element's own Class, and reads back the same;
- busArray.mat with its object table re-pointed: two MathWorks classes, a user's class
beside one, and a later element of no class;
- controls: a uniform array (busArray, ndNested) and an enumeration Value carry no
element class.
No existing test changed. Three tests that walk every fixture (noObjectObject,
mcosDecodeWarnings, usageScanSummary) now also open configset.mat, and pass.
Not in scope, recorded in DESIGN.md: a .mat variable that is itself a mixed-class object
array is refused whole, by decodeMcosVariables's check that every object is of the
declared class. That is read from the code. It was not measured on a MATLAB-written
file, and no such variable is in the corpus or the fixtures.
…y its elements Review of F1-47 found that removing componentsBaseOf from McosParser.resolveValue left the whole suite green. On configset.mat as MATLAB wrote it, every component is one that CONFIG_COMPONENTS lists, so the shared element rule (arrayClassOf) already names both config sets' Components, and their RTWCC's, Simulink.ConfigComponent. The call was live but redundant on every fixture and corpus file. The case it exists for is a component the list does not hold, such as the CoderTarget.SettingsController and SSC.SimscapeCC of the corpus binary dictionaries. No test had one. mcosMixedObjectArray.test.ts now re-points configset.mat's class table one word at a time, as it already does to busArray.mat: - SlCovCC.ConfigComp loses its package, so the last component of both config sets is ConfigComp, a class the list does not hold and the shared rule counts as not MathWorks'. arrayClassOf would name the array Simulink.SolverCC, after its first element. - cs's Simulink.GRTTargetCC is re-pointed at that class, so its RTWCC's Components is a CodeAppCC beside a ConfigComp. arrayClassOf would name the array Simulink.CodeAppCC. Both arrays still read as Simulink.ConfigComponent, with every element under its own class. A precondition asserts the tables and what arrayClassOf alone would say. A control re-points mixed's Kids, a property of an NdHolder, at the same two classes, and that array is read by the shared rule. With `componentsBaseOf(owner, propName) ??` removed from src, 2 of the 4 new tests fail (cs: expected 'Simulink.SolverCC' to be 'Simulink.ConfigComponent'; expected 'Simulink.CodeAppCC' to be 'Simulink.ConfigComponent'). At 9c247d6 all four pass.
…e again C-033 made McosParser.resolveValue's logical arm return [] for every empty logical, before it read the shape. Base (5cd013c) gave an empty logical of rank 3 or more the typed literal, {_type: 'logical', _value: 'Matrix(2,0,2)\n[[]; []]'}, which shows as a <2x0x2 logical>. The [] reads as a double 0x0. No fixture or corpus file reaches it, and no test pinned it; the review found it by reading the code. The early return now applies only where base's own shape test also gave a plain []. That test is the one base used to choose the typed literal, and it is restored unchanged. So every empty base gave [] still gets []: an empty logical of rank 2, and an empty of every numeric class. Every empty base typed is typed again. Nothing that is not empty changes. mcosEmptyLogical.test.ts takes typed_objects.mat's pInt16Nd, MATLAB's int16 2x2x2, and respells its dims subelement (same length) and sets its logical bit, as mcosColumnVector.test.ts does to ParamMat.mat. A logical 2x0x2, 0x2x2 and 2x2x0 are typed and show as logical at their shape. Those 2 tests fail at 9c247d6 ("className": "double", dims [0, 0]) and pass here. The controls hold at both: an int16 2x0x2 and logical 0x0, 1x0, 0x1, 2x0 and 0x3 are []; a logical 2x2x2 is typed. DESIGN.md gets back a one-bullet limitation for what C-033 leaves out. An empty numeric or logical array of a class other than double, in an MCOS object, reads as a double 0x0. The bullet that covered it went out whole with C-033.
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.
Implements the maintainer's 2026-10-16 group E ruling.
This PR covers two theme 2 items from the backlog, one commit each. The ruling for theme 2 says: "follow MATLAB, and measure it first (
mw -using Bmain matlab -nodesktop -batch …) where it is unmeasured." The maintainer agreed each item's recommendation ("Agree your suggestions ... Add unit tests to lock down the fix."). Each recommendation is quoted below. Every new test was written first and runs the real reader or session. Each commit includes its own rebuiltdist/, andnpm run verifywas green at each commit.Gate
9c247d6):rm -rf dist && npm run buildleaves the tree clean.npm run verifyreturns rc 0. Test files: 254 passed / 1 skipped. Tests: 8390 passed / 26 skipped. The typecheck, smoke, pack, leak and browser checks pass.3decebe, which took Duplicate-name ids, a model's MAT workspace for Usage, a lone config set, and struct-array element names #127 after this branch was cut from5cd013c):git merge-treereports no conflict. In the merged tree, the committeddist/matches a fresh build. Verify there: 256 files / 8491 passed / 26 skipped.C-033: an MCOS object's non-double numbers keep their class, and a single shows MATLAB's digits (
dddd172)Recommendation: "(a), limited to non-double classes so double output (and the tests pinning it) are untouched, and decided together with the single-precision display limitation." Option (a) is: "Emit the dictionary spelling for non-double classes ... and the logical typed literal for logical vectors. Doubles stay bare."
What was wrong: in a
.mator a model workspace, a Simulink.Parameter whose Value isint32([100 200 300 400])showed as class double, where both dictionary formats show int32. A logical row also showed as double.single(3.3)showed3.299999952316284, where both dictionaries show3.3. The corpus's MATLAB-written twins (param_*, parity_*) show all of these.Root cause:
McosParser.resolveValuepassed every numeric scalar and row through bare, andbuildMatrixValuestamped_type: 'double'on every column and matrix. The node layer reads a bare number as a double. Separately,MatParserread a single as the double thatgetFloat32widens it to.Fix:
McosParser.resolveValue: a real value of any class other than double now gets the binary reader's typed spelling, as a scalar, a row or aMatrix(). A logical row gets the typed logical literal. A double is unchanged. Two readers of resolved values are adjusted to match:functionHandleTextreads its marker from the raw cell, andenumerationValuereads a typedint32.Data.MatParser: an array of class single (full, complex or sparse) gives each element in MATLAB's digits (singleDigits). This is gated on the class, not on the miSINGLE storage type, so a double stored as miSINGLE keeps its double value.MATLAB measurement (R2027a, 3 sessions): a 4,086-pattern float32 sweep and 7 exact ties, in both dictionary formats. MATLAB writes C
%.6gwhen that gives back the same single, otherwise%.9gwith ties to even (1000000.125 → 1000000.12), and%.10gfor realmax('single'). This is not "the shortest decimal": MATLAB writes single(1/3) as 0.333333343. Over the 4,082 patterns kept in the fixture, the shortest-decimal rule misses 3,520 and this rule misses 0. It also misses 0 of the 127,743 non-zero singles in a customer text dictionary in the corpus. The rule is lossless. An exact-arithmetic fast path gives the same numbers as a string reference on every 31st bit pattern. A million singles take 75 ms, against 400 ms withtoPrecisionalone.Tests (68 fail on
5cd013c, each on the old class or digits):typedMcosFixtures.test.ts, 54 failing before, with new MATLAB-made fixtures (make_typed_fixtures.m:typed_objects.mat,typed_ws.slx,typed_mcos.sldd,typed_mcos_binary.sldd, and a truth file). It checks that:.mat's numbers are MATLAB's, bit for bit;.matreopens in each dictionary as MATLAB's own entry. Before the fix, the binary Value tag readClass="double">-42.0where MATLAB wroteClass="int8">-42.It also has a guard that keeps each int32 enumeral's number.
matSingleDigits.test.ts, 7 failing before. It builds MAT bytes by hand and covers plain, complex and sparse singles,decodeMatStream, ties, and a lossless sweep. Its controls are exact values, Inf/NaN/-0, a double stored as miSINGLE, and doubles.mcosColumnVector.test.ts, 6 failing before. It patches ParamMat.mat's class byte to get int32 1x6, int16 6x1, uint8 1x1, single 2x3, uint64 1x3 and logical 1x6, with double and logical scalars as controls.mcosParser.test.ts, 1 failing before (below).Existing tests changed, each because it pinned exactly what the ruling removes:
mcosColumnVector.test.ts, "control: a logical 1x6 is still the bare row of booleans it was": it now expects the typed logical row, shown as class logical. A bare boolean list is what showed as double.mcosParser.test.ts, "array of logicals resolves as boolean[], not number[] (Dimensions)": it now expects{_type: 'logical', _value: '[0, 0]'}, for the same reason.complexMcosValue.test.ts: a comment only. The case still tests the formatter on a widened double, which a.matno longer hands over.Corpus (168
.mat/.slxfiles, main against this commit): 82 rows change class (param_* int32 and logical, and topMat.mat's uint64mID). 88 rows change digits (allMidiSeq.mat 74, parity_* 10, param_* 4). Nothing else changes, and no file newly fails to load.Docs: two DESIGN.md known limitations are removed, because this commit fixes them.
Left out: an empty array of a class other than double still resolves to
[], a double 0x0. The binary reader's classed empty carriesBINARY_EMPTY_KEY, and what that key means for a value read from a.matis a separate question. The opaque-node summary (case 2 ofmodelOpaqueMcosVariable) for an unmodelled class whose Value is a non-double scalar is not touched: no corpus file or fixture reaches it. The binary fixture leaves out the sweep, becausebinarySaveKeepsUneditededits every element and ran past its 120 s timeout with it.F1-47: a mixed-class object array in a
.matkeeps each element's class (9c247d6)Recommendation: "(a). It is the D6/D15 rule applied at the reader where the defect starts (the standing rule is to fix at the root cause), and one shared rule cannot drift between the two readers." Option (a) is: "Move arrayClassOf, heterogeneousBaseOf, componentsBaseOf and CONFIG_COMPONENTS into one shared parser module, and call it from both BinarySlddParser and McosParser, with a per-element _object_class where it differs."
What was wrong: an object array held by an MCOS property, in a
.mator a model workspace, was named after its first element, and no element kept its own class. A config set's Components read as a Simulink.SolverCC array, and a copy pasted into a dictionary wrote all nine components as Simulink.SolverCC. A dexdata.DerivedSignal beside a Simulink.Signal read as two DerivedSignals.Root cause:
resolveValuetook_array_classfrom the first element's class and built every element as{_properties}only. The D6/D15 rules that the binary reader uses were private toBinarySlddParser.Fix: a new module,
src/datamodel/parser/objectArrayClass.ts, holdsarrayClassOf,heterogeneousBaseOf,componentsBaseOfand their lists, moved unchanged except for comment wording.BinarySlddParserimports it and behaves as before;objectArrayElementClass.test.tsis unchanged and green. InMcosParser.resolveValue:componentsBaseOf(owner, propName), or otherwise byarrayClassOfover the elements' classes;_object_class, placed first;MATLAB measurement (R2027a): a new fixture (
make_configset_fixture.m→configset.mat,configset.truth.json) holdscs(grt.tlc),csErt(ert.tlc) andmixed(Kids =[dexdata.DerivedSignal, Simulink.Signal]). MATLAB'sclass()isSimulink.ConfigComponentfor both config sets' Components and their RTWCC's Components, andSimulink.Signalformixed.Kids. That is the class the shared rule names in each case.Tests (written first):
objectArrayClass.test.ts(6 tests) does not load ondddd172, because the module does not exist.mcosMixedObjectArray.test.ts: 10 of its 14 tests fail ondddd172, each on the old class. One example: SolverCC where ConfigComponent is expected. It covers:configset.matagainst MATLAB's truth;<1x2 Simulink.Signal>, with an ObjectNode and a SignalNode under it;<Element Class>written, and what reads back;busArray.matwith its object table re-pointed: two MathWorks classes, a user's class beside one, and a classless later element.The other 4 tests are a precondition and controls (a uniform array, and an enumeration Value), and they pass both before and after.
Existing tests changed: none. Three tests that walk every fixture now also open
configset.mat, and they pass.Corpus (168 files, the parent commit against this one): of 401 decoded MCOS variables, exactly 2 change:
csin findVar/dConfig.mat and in models/topMat.mat. The node-tree walk is the same on all 419,476 rows, and no file newly fails to load.Left out: no binary-dictionary fixture with a ConfigSet was added. A new binary fixture would also be walked by
binarySaveKeepsUnedited, which ran past its timeout in C-033. The test pastes into the existingsparse/sparse_binary.slddinstead. A component's Name is not saved in the.mat's MCOS, so names are not asserted. A top-level.matvariable that is itself a mixed-class object array is still refused whole bydecodeMcosVariables's class check. That case comes from reading the code and was not measured; no file in the corpus or fixtures has one. It is recorded in DESIGN.md.Review follow-ups (9e22f8f, 881e8a9)
These are the reviewers' three non-blocking notes, each now locked down by a unit test.
componentsBaseOfis now load-bearing in a test (9e22f8f, test only).McosParser.resolveValueleft the whole suite green. Onconfigset.matevery component is inCONFIG_COMPONENTS, soarrayClassOfalready named both arraysSimulink.ConfigComponent.mcosMixedObjectArray.test.tsnow re-pointsconfigset.mat's class table, the way it already does forbusArray.mat.SlCovCC.ConfigCompbecomesConfigComp, which is not in the list (the same situation as the corpus's CoderTarget and SSC components). With that change,arrayClassOfalone would name cs's ComponentsSimulink.SolverCCand its RTWCC'sSimulink.CodeAppCC.Simulink.ConfigComponent. A control checks that the same classes under an NdHolder'sKidsstill go through the shared rule.componentsBaseOf(owner, propName) ??removed from src, 2 of the 4 new tests fail. All 4 pass at 9c247d6.if (val.length === 0) return []came before the shape test. So a logical 2x0x2 in an MCOS property became[], which reads as a double 0x0. At base it was{_type: 'logical', _value: 'Matrix(2,0,2)\n[[]; []]'}, shown as<2x0x2 logical>.[]. Every other case is unchanged: the rank-2 empty logical, every numeric empty, and every non-empty value.mcosEmptyLogical.test.ts:typed_objects.mat's int16 2x2x2pInt16Nd, with its dims respelled (same length) and the logical bit set.Gates at 881e8a9:
rm -rf dist && npm run buildleaves the tree clean.npm run verifypasses (rc 0): 255 files passed / 1 skipped, 8400 tests passed / 26 skipped. That is the 10 new tests on top of 8390.Head and gate after review: head
881e8a9(two commits on top of9c247d6:9e22f8ftest-only,881e8a9src + dist + test + DESIGN.md).npm run verify: 8400 passed / 26 skipped. Merged with main3decebe: 8501 passed / 26 skipped. C-033's left-out classed empty now excludes a logical of rank 3 or more, which keeps its typed shape again.