Skip to content

fix(core-engine): matlab's return channel is not a state mutation (#2654) - #2703

Merged
squid-protocol merged 2 commits into
mainfrom
fix/2654-matlab-return-channel
Sep 4, 2026
Merged

fix(core-engine): matlab's return channel is not a state mutation (#2654)#2703
squid-protocol merged 2 commits into
mainfrom
fix/2654-matlab-return-channel

Conversation

@squid-protocol

Copy link
Copy Markdown
Owner

Answers #2654, which was filed as a question: should MATLAB's mandatory out = x return-convention assignments count as state_mutation? The answer is no, and the discriminator is narrower than either option the issue sketched.

Why it is a defect, not intended morphology

MATLAB has no return <value> statement. A function result is an assignment to a name declared in function [out] = f(...). The rosetta corpus makes this exact: c writes return env;, go writes return env, matlab writes out = env; — the same statement, and matlab is the only language in the registry charged for it.

Re-derived against the current corpus (the issue's +450% was pre-Batch-A; the medians have moved since):

value corpus median
matlab state_mutation 25 2

19 raw hits, of which 15 are the return convention; #2546's per-function ×3 flux weighting turns those into the whole cell.

Why "exclude all writes to output variables" would be wrong

runica.m uses weights as the accumulator for an entire ICA loop and assigns it repeatedly. That is genuine working state. The discriminator is not on the assignment's line — it is whether the enclosing function ever reads the name back:

  • every occurrence a bare LHS (out = env;, never mentioned again) → the return channel, dropped
  • read in an expression, out = out + i, out(i) = x, passed as an argument → working state, every hit kept

Implemented as a matlab_return_channel entry in #2674's registry-declared _scope_filters, so the registry stays data and an unknown filter name can only restore the pre-filter count, never zero a metric.

Measurements (Prism code streams, what coding_analysis actually sees)

keyword-rosetta — 19 → 4, so the cell moves 25 → 4, the value haskell/m4/perl/dockerfile already sit at:

file before after
main.m 12 0
a.m 3 0
b.m 6 3
c.m 4 1

rosetta_audit.py: 45/46 PASS, matlab the single regression — this is the intentional corpus-visible change the rosetta:rebless-owed label is for. The corpus re-bless follows against engine main per docs/self_scan/ROSETTA_AUDIT.md.

language-crucible (eeglab) — 1321 → 1306 (1.1%). Every one of the 15 drops is a terminal binding: com = '', varargout = { [] [] 0 {} [] }, name = dirName;, h = uimenu(varargin{:});, this = class(TMP, 'eegobj'), command = sprintf(...). runica.m — the file that works its outputs hardest — is untouched at 440 → 440. In parsepluginname the two outputs are judged independently: write-only name drops, read-back vers keeps every hit.

Golden master

Re-blessed in both modes. 72 diffs each: 43 are the benign corpus-wide topological re-solve, 29 substantive — all inside matlab/eeglab apart from the two whole-corpus health averages they roll up into. Risk exposures fall slightly for real MATLAB (eeglab cognitive_load 62.99 → 60.13, state_flux 85.71 → 84.81), which is the intended direction: less phantom mutation means less phantom debt.

Gauntlet

  • suite 7533 passed, 3 skipped, 9 xfailed
  • audit_check.py all clear (ruff 70-finding baseline, mypy 2-error baseline, dead-key, ast-accuracy)
  • tree_sitter_accuracy_audit --all --ci 30/30 OK
  • tri_comparison_chart --all --ci 3/3 OK
  • crucible_check.py PASS in both modes after the bless
  • 13 new tests in test_matlab_strict.py, including the accumulator/indexed/multi-output cases, the unknown-filter-name fallback, and a linearity check

Note for Batch D

main.m loses its state_mutation entirely, and its notes call it "the cleanest demonstration in the corpus" of the ×3 per-function flux weighting. That demonstration should be re-planted with a genuine mutation in probe_branch as part of the corpus re-bless — flagged there rather than fixed here.

Closes #2654

🤖 Generated with Claude Code

)

MATLAB has no `return <value>` statement. A function result IS an assignment
to a variable named in the `function [out] = f(...)` signature, so `out = env;`
is the exact statement c/go/java write as `return env;`. state_mutation's rule
counts every assignment, which made matlab the one language in the registry
that pays for returning: on the keyword-rosetta corpus 15 of 19 raw hits were
the return convention, and with #2546's per-function x3 flux weighting that is
25 of the cell's 25 -- against a corpus median of 2, the widest deviation in
the whole bias table.

Excluding every write to an output variable would be wrong the other way.
runica.m uses `weights` as the accumulator for a whole ICA loop and assigns it
repeatedly; that is genuine working state. The discriminator is not anything on
the assignment's own line but whether the enclosing function ever READS the name
back: a name whose every occurrence in the body is a bare left-hand side is
purely the return channel, while a read, a self-reference (`out = out + i`) or a
structured write (`out(i) = x`) makes it ordinary state and keeps all its hits.

Implemented as a `matlab_return_channel` entry in #2674's registry-declared
`_scope_filters`, so the registry stays data and an unknown filter name can only
restore the pre-filter count, never zero the metric.

Measured over Prism code streams, which is what coding_analysis actually sees:

  * keyword-rosetta: 19 hits -> 4, i.e. the cell moves 25 -> 4 (median 2, the
    same value haskell/m4/perl/dockerfile already sit at). All four files move
    and no other language does -- rosetta_audit reports 45/46 PASS with matlab
    the single regression, so this PR carries `rosetta:rebless-owed` and the
    corpus re-bless follows against engine main.
  * language-crucible eeglab: 1321 -> 1306 (1.1%). Every one of the 15 drops is
    a terminal binding (`com = ''`, `varargout = {...}`, `name = dirName;`,
    `h = uimenu(...)`, `this = class(TMP, 'eegobj')`); runica.m, the file that
    works its outputs hardest, is byte-identical at 440.

Golden master re-blessed in both modes: 72 diffs each, 43 of them the benign
corpus-wide topological re-solve and 29 substantive -- all inside matlab/eeglab
apart from the two whole-corpus health averages those roll up into. Risk
exposures fall slightly for real MATLAB (eeglab cognitive_load 62.99 -> 60.13),
which is the intended direction.

Suite 7533 green, audit_check clear (ruff/mypy/dead-key/ast), tree-sitter 30/30
and tri-comparison 3/3 OK.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@squid-protocol squid-protocol added the rosetta:rebless-owed Intentionally moves keyword-rosetta counts; audit warns, corpus re-blesses after merge label Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🐦‍⬛ Muninn Security Scan

✅ No security issues found.

🐦‍⬛ Powered by Muninn · Skald Lab

`rosetta:rebless-owed` is what turns an intentional corpus-visible change's
regressions into warnings, but `rosetta-audit.yml` only listened for the default
`pull_request` types, so the label could only ever be read by the run that
OPENED the PR. `gh pr create --label` attaches labels after that payload is
built, so #2703 -- the first PR to actually use the flow #2699 introduced --
read zero labels and went red on a regression its author had already declared,
with no way to re-trigger short of an empty commit.

Adds `labeled`/`unlabeled` to the trigger types and documents the gotcha in
ROSETTA_AUDIT.md's decision list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rosetta:rebless-owed Intentionally moves keyword-rosetta counts; audit warns, corpus re-blesses after merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

matlab: mandatory 'out = x' return-convention assignments count as state_mutation (+450% vs median) — intended?

1 participant