Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/rosetta-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ name: rosetta-audit

on:
pull_request:
# `labeled` matters: `rosetta:rebless-owed` is what turns an intentional
# corpus-visible change's regressions into warnings, and without this type
# the audit only ever runs on the event that OPENED the PR. gh's
# `pr create --label` attaches the label after that payload is built, so
# the first run of #2703 read zero labels and failed red on a regression
# the author had already declared -- and no later label change could
# re-trigger it. `unlabeled` is here for the same reason in reverse.
types: [opened, synchronize, reopened, labeled, unlabeled]
branches: [main, v6-dev]
paths:
- "gitgalaxy/**"
Expand Down
7 changes: 6 additions & 1 deletion docs/self_scan/ROSETTA_AUDIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ Decide which of these you are in:
2. **Intentional, corpus-visible improvement** (a rule fix or addition, a stripper change).
The expected values live in the corpus repo, so:
- add the **`rosetta:rebless-owed`** label to this PR — the audit reports the regressions
as warnings and goes green, with the languages still listed in the summary;
as warnings and goes green, with the languages still listed in the summary. Adding the
label re-runs the audit (the workflow listens for `labeled`), so an already-red run
turns green on its own; you do not need to push an empty commit. Note that
`gh pr create --label` attaches the label *after* the `opened` payload is built, so the
very first run of a new PR reads no labels and goes red — the `labeled` event that
follows is the one that counts;
- merge;
- open the re-bless PR in keyword-rosetta **against engine main** (manifests + ledger
per its `docs/GATING.md`). Its `verify.yml` checks out engine main, so it is green by
Expand Down
93 changes: 93 additions & 0 deletions gitgalaxy/core/detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -5594,6 +5594,18 @@ def _matching_paren_end(self, text: str, open_idx: int) -> int:
_LISP_FORM_HEAD: ClassVar[re.Pattern[str]] = re.compile(r'[ \t\r\n]*([^\s()\[\];"]{1,200})')
_LISP_LET_FAMILY: ClassVar[frozenset[str]] = frozenset({"let", "let*", "letrec", "letrec*"})

# #2654: every MATLAB function declaration, whether or not it declares
# outputs. The output list is optional so a `function helper(x)` still
# opens a span -- otherwise its body would be scanned as part of the
# PRECEDING function's, and a same-named local there could be dropped as
# if it were that function's return channel. Both name lists are bounded
# (no adjacent unbounded quantifiers), so the scan stays linear.
_MATLAB_FUNC_DECL: ClassVar[re.Pattern[str]] = re.compile(
r"^[ \t]*function\b"
r"(?:[ \t]*(?:\[(?P<outs>[^\]\n]{0,400})\]|(?P<out1>[a-zA-Z_]\w{0,127}))[ \t]*=(?![=]))?",
re.M,
)

def _apply_scope_filter(
self,
filter_name: str,
Expand Down Expand Up @@ -5621,11 +5633,92 @@ def _apply_scope_filter(
if paren != -1 and paren in keep:
kept.append(m)
return kept
if filter_name == "matlab_return_channel":
if filter_name not in cache:
cache[filter_name] = self._matlab_return_channel_offsets(code)
drop = cache[filter_name]
kept = []
for m in matches:
# The assignment alternative is `^[ \t]*` anchored, so its
# identifier is the line's first non-space character; the
# `clear`/`clearvars` alternative is unanchored and can only
# start further in, which is what keeps a `out = 1; clear y`
# line from losing its cleanup hit along with its binding.
idx = m.start()
while idx < len(code) and code[idx] in " \t":
idx += 1
if idx not in drop:
kept.append(m)
return kept
self.logger.warning(
f"[DIAGNOSTIC] Unknown scope filter '{filter_name}' declared for '{seg_lang}::{rule_name}'. Ignoring."
)
return matches

def _matlab_return_channel_offsets(self, code: str) -> set[int]:
"""
Offsets of every bare assignment to a MATLAB output variable that its
function only ever WRITES -- the return-channel bindings (#2654).

MATLAB has no `return <value>`: a result is an assignment to a name in
the `function [out] = f(...)` signature, so `out = env;` is the exact
statement c/go/java write as `return env;` for zero state_mutation.
Dropping every write to an output variable would be wrong the other
way -- runica.m uses `weights` as the working accumulator for a whole
ICA loop and assigns it repeatedly -- so the discriminator is whether
the body ever reads the name back. A name whose every occurrence in
the body is a bare left-hand side (`name =`, no subscript, no field,
no mention on the right) is pure return channel; one that appears
anywhere else -- read in an expression, `out(i) = x`, `out = out + 1`,
passed as an argument -- is working state and keeps all of its hits.

A function's body runs to the next `function` declaration. For a
nested function that truncates the parent's body early, which can only
make the parent's name look MORE read (the nested span is scanned
under the nested declaration) and so only ever KEEPS hits: the failure
direction is the pre-filter count, never a zeroed metric.

Measured over Prism code streams: the rosetta corpus drops 15 of 19
hits (25 -> 4 once the per-function x3 flux weighting is applied,
against a corpus median of 2); language-crucible's eeglab drops 15 of
1321 (1.1%), each a terminal `com = ''` / `varargout = {...}` /
`name = dirName;` binding, with `weights`, `y` and `EEG` untouched.
"""
decls = list(self._MATLAB_FUNC_DECL.finditer(code))
if not decls:
return set()

offsets: set[int] = set()
for i, decl in enumerate(decls):
raw_outs = decl.group("outs")
if raw_outs is not None:
names = {t.strip() for t in raw_outs.split(",") if t.strip()}
elif decl.group("out1"):
names = {decl.group("out1")}
else:
continue

body_start = decl.end()
body_end = decls[i + 1].start() if i + 1 < len(decls) else len(code)
body = code[body_start:body_end]

for name in names:
if not name.isidentifier():
continue
quoted = re.escape(name)
write = re.compile(rf"^[ \t]*({quoted})[ \t]*=(?![=])", re.M)
write_starts = {m.start(1) for m in write.finditer(body)}
if not write_starts:
continue
# Any occurrence that is not one of those bare left-hand sides
# is a read (or a structured write), which disqualifies the
# whole name -- MATLAB's output variable is then ordinary
# working state, not just the return channel.
if all(m.start() in write_starts for m in re.finditer(rf"\b{quoted}\b", body)):
offsets.update(body_start + start for start in write_starts)

return offsets

def _lisp_module_level_define_offsets(self, code: str) -> set[int]:
"""
Offsets of the "(" of every `(define ...)` in `code` whose nearest
Expand Down
17 changes: 17 additions & 0 deletions gitgalaxy/standards/language_standards/languages/matlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,23 @@
r"[ \t]*=[ \t]*[^=]|\b(?:clear|clearvars)\b",
re.M,
),
# #2654: MATLAB has no `return <value>` statement -- a function result
# IS an assignment to a variable named in the `function [out] = f(...)`
# signature. So the rule above charged every language-crucible and
# rosetta function one state_mutation just for RETURNING, the one
# language in the registry that pays for it (c/go/java's `return env;`
# costs nothing for the identical statement). The discriminator is not
# anything on the assignment's own line -- it is whether the enclosing
# function ever READS the variable back. `out = env;` in a body that
# never mentions `out` again is the return channel; runica.m's
# `weights = startweights;` in a body that reads `weights` throughout
# is genuine working state and still counts, as does any indexed or
# self-referential write (`out(i) = x`, `out = out + i`). See
# `_matlab_return_channel_offsets` in detector.py. Measured: the
# rosetta corpus drops 15 of 19 hits (all four files' `out = <arg>;`
# returns), eeglab drops 15 of 1321 (1.1%) and every one of those is a
# terminal `com = ''` / `varargout = {...}` / `h = uimenu(...)` binding.
"_scope_filters": {"state_mutation": "matlab_return_channel"},
# 12. dead_code (Commented Logic / Deprecated Trails)
"dead_code": re.compile(r"^[ \t]*%[ \t]*(?:if|for|while|function|classdef)\b", re.M),
# doc: Standard MATLAB Help text (`%%` sections) or typed annotations.
Expand Down
129 changes: 129 additions & 0 deletions tests/extraction/languages/test_matlab_strict.py
Original file line number Diff line number Diff line change
Expand Up @@ -541,3 +541,132 @@ def test_matlab_redos_immunity_sweep():
assert MATLAB_RULES["func_start"].search("function y = foo(x)")
assert MATLAB_RULES["class_start"].search("classdef Foo")
assert MATLAB_RULES["state_mutation"].search("data(idx(1)) = value;")


# ==============================================================================
# #2654: state_mutation -- MATLAB's return channel is an assignment
# ==============================================================================
# MATLAB has no `return <value>` statement: a result is an assignment to a name
# declared in `function [out] = f(...)`. The bare regex therefore charged every
# function one state_mutation just for returning -- the only language in the
# registry that pays for the statement c/go/java write as `return env;` for
# free. The discriminator is not on the assignment's line but in the enclosing
# function: a name the body never READS back is the return channel; one the
# body works with (`out = out + 1`, `out(i) = x`, `y` in runica.m's ICA loop)
# is genuine state. detector.py's coding_analysis applies the registry-declared
# `matlab_return_channel` scope filter, so these go through the real extractor.


def _matlab_state(code: str) -> int:
"""
RAW state_mutation hits, straight out of coding_analysis. Deliberately not
`splice()["equations"]`, whose value has #2546's per-function x3 proximity
flux applied on top -- that weighting is what turns matlab's four surviving
rosetta hits into a cell value, and mixing it in here would hide which of
the two effects a regression came from.
"""
from gitgalaxy.core.detector import StructuralExtractor

counts, *_ = StructuralExtractor("matlab", LANGUAGE_DEFINITIONS).coding_analysis([("matlab", code, 0)])
return counts["state_mutation"]


def test_matlab_scope_filter_is_declared_for_state_mutation():
assert MATLAB_RULES["_scope_filters"] == {"state_mutation": "matlab_return_channel"}


def test_matlab_return_convention_assignment_is_not_a_mutation():
"""The issue's own example: `out = env;` IS `return env;`."""
code = "function out = probe_globals(env)\nout = env;\nend\n"
assert MATLAB_RULES["state_mutation"].search(code), "sanity: the bare regex still matches the binding"
assert _matlab_state(code) == 0


def test_matlab_branch_exclusive_returns_are_all_return_channel():
"""Three mutually exclusive `out = k` are three `return k`, not three mutations."""
code = "function out = probe_branch(flag)\nif flag > 0\nout = 1;\nelseif flag < 0\nout = 2;\nelse\nout = 3;\nend\nend\n"
assert _matlab_state(code) == 0


def test_matlab_output_variable_read_back_is_working_state():
"""runica.m's shape: an output used as the accumulator keeps every hit."""
accumulate = "function out = total(n)\nout = 0;\nfor i = 1:n\nout = out + i;\nend\nend\n"
assert _matlab_state(accumulate) == 2
indexed = "function out = fill(n)\nout = zeros(1, n);\nout(1) = 5;\nend\n"
assert _matlab_state(indexed) == 2
passed_along = "function out = wrap(x)\nout = x;\ndisp(out);\nend\n"
assert _matlab_state(passed_along) == 1


def test_matlab_multiple_outputs_are_judged_independently():
"""parsepluginname's shape: `name` is write-only, `vers` is read back."""
code = (
"function [name, vers] = parse(dirName)\n"
"name = dirName;\n"
"vers = '';\n"
"vers(vers == '_') = '.';\n"
"end\n"
)
# `name` drops; `vers`'s bare binding AND its indexed write both stay.
assert _matlab_state(code) == 2


def test_matlab_non_output_locals_still_count():
code = "function out = probe_debt(level)\nhack_level = level;\nout = hack_level;\nend\n"
assert _matlab_state(code) == 1


def test_matlab_script_level_and_void_functions_are_untouched():
"""No declared output means nothing to drop; a void function opens its own span."""
assert _matlab_state("x = 5;\ny = 6;\n") == 2
# `out` here belongs to the void helper, not to the preceding function.
code = "function out = f(a)\nout = a;\nend\nfunction helper(b)\nout = b;\nend\n"
assert _matlab_state(code) == 1


def test_matlab_clear_on_a_dropped_binding_line_keeps_its_own_hit():
"""The `clear` alternative is unanchored; it must survive its line's binding being dropped."""
code = "function out = f(a)\nout = a; clear scratch\nend\n"
assert _matlab_state(code) == 1


def test_matlab_return_channel_filter_keeps_counts_and_locations_consistent():
from gitgalaxy.core.detector import StructuralExtractor

d = StructuralExtractor("matlab", LANGUAGE_DEFINITIONS)
code = "function out = f(a)\nout = a;\nend\nfunction out = g(b)\nnote = b;\nout = note;\nend\n"
counts, _mit, spatial_maps, _parents, locations = d.coding_analysis([("matlab", code, 0)])
assert counts["state_mutation"] == 1
assert len(spatial_maps[0]["state_mutation"]) == 1
assert locations["state_mutation"] == [5]


def test_matlab_unknown_scope_filter_name_is_ignored_not_zeroed():
import copy

from gitgalaxy.core.detector import StructuralExtractor

defs = copy.deepcopy(LANGUAGE_DEFINITIONS)
defs["matlab"]["rules"]["_scope_filters"] = {"state_mutation": "no-such-filter"}
code = "function out = f(a)\nout = a;\nend\n"
counts, *_ = StructuralExtractor("matlab", defs).coding_analysis([("matlab", code, 0)])
assert counts["state_mutation"] == 1


def test_matlab_return_channel_scan_is_linear_on_pathological_input():
"""The scan is a bounded declaration regex plus per-name passes, not backtracking."""
import time

from gitgalaxy.core.detector import StructuralExtractor

d = StructuralExtractor("matlab", LANGUAGE_DEFINITIONS)
payloads = [
"function out = f(a)\n" + "out = a;\n" * 20000 + "end\n",
"function out = f(a)\n" + "out = out + a;\n" * 20000 + "end\n",
("function out = f(a)\nout = a;\nend\n" * 5000),
"function [" + ",".join(f"o{i}" for i in range(40)) + "] = f(a)\n" + "o1 = a;\n" * 5000 + "end\n",
]
for payload in payloads:
start = time.perf_counter()
d.coding_analysis([("matlab", payload, 0)])
assert time.perf_counter() - start < 10.0, "return-channel scan went superlinear"
Loading
Loading