Skip to content

m4 avg_func_args counts $1 references, not declared positions: m4 lacks shell's _args_findall_max_groups #2784

Closed

Description

@squid-protocol

Found by the 46-language audit in docs/args_rule_contract.md, written for #2773.

The contract

args matches the parameters a callable declares.

Corollary 3: in a language with no formal parameter list, $1/$@ in a body is the only
parameter surface there is -- but a count of those references measures how often the body reads
its arguments, not how many it takes.

Measured

keyword-rosetta/data/m4, args against func_start, over the code stream:

file m4_defines args why
a.m4 3 5 probe_test and probe_safety each reference $1 twice
b.m4 3 4 one macro references $1 twice
c.m4 3 3 exact
main.m4 4 6 two macros reference $1 twice
total 13 18 (+38%) 5 duplicate references
m4_define(probe_test, [AT_SETUP($1) AT_CHECK($1)])      # 1 parameter, args records 2

language-crucible/data/m4: 70 args against 39 func_start (a/f 1.79).

The cause and the fix

"args": re.compile(r"\$[0-9]+|\$[@*#]"),     # m4 -- no helper

shell has exactly the same rule shape and exactly the same problem, and already solves it:

"args": re.compile(r"(\$(?:[1-9]|\{#?[1-9][0-9]*…\}|@|\*|#)|\"\$@\"|\"\$\*\")"),
"_args_findall_max_groups": {1},

_args_findall_max_groups makes _calculate_block_metrics take the highest position
referenced
inside the block rather than the number of references, so $1 … $1 … $3 is 3
parameters, not 3 references. m4 declares no such helper, so _calculate_block_metrics falls
through to the generic counter and avg_func_args inherits the reference count.

Two pieces of work, and they are separable:

  1. Per-function -- give m4 _args_findall_max_groups (and a capture group to hang it on).
    Mechanical, mirrors shell exactly. $@/$* need whatever shell's helper already does for them.
  2. File-level struct_args -- the raw rule count has no per-block context to take a maximum
    over, so it stays a reference count. shell, makefile and yacc all share this and all
    currently read 13 on the rosetta shells only because those shells happen to reference each
    position once. Worth deciding explicitly whether that is accepted (and ledgered under
    args-no-parameter-surface-morphology) or whether the positional family needs a per-macro
    dedupe -- m4 is the language that exposes it, but the question is the family's.

Corpus pairing

args is planted at 13 (one parameter per probe, SPEC.md). keyword-rosetta/data/m4 needs no
authoring change for part 1 -- the fix should land it on 13 exactly.

Part of #2669 (rosetta[m4] tracking issue).

Activity

  1. added
    bugUnintended behavior or logic failure in the engine
    metricsHeuristics, risk exposures, and topological math updates
    core-engineModifications to the central physics and parsing engine
    on Sep 6, 2026
  2. squid-protocol commented on Sep 6, 2026

    @squid-protocol
    OwnerAuthor

    Narrowing this — half of it was already settled and I missed it.

    keyword-rosetta's deviation_ledger.json carries m4-parameters-are-use-sites (disposition intended-morphology, validated 2026-09-04):

    m4 args 18 vs a median of 13 [...] m4 macros have no parameter list — define(probe_globals, ...) names no parameters and $1 inside the body IS the parameter, so the rule counts one arg per USE, not per declaration [...] The plant is correct (each macro takes exactly one argument); the count is the language's syntax, and equalising it would mean writing macros that never reference their own argument.

    So the headline +38% is not a defect and this issue should not be read as asking for m4's file-level args to reach 13. I filed it without checking the ledger; that verdict stands and I am not proposing to overturn it.

    What is still genuinely open is the part the ledger does not address: avg_func_args.

    shell has the same morphology (no parameter list, $1 in the body is the parameter) and the same file-level behaviour, but it declares _args_findall_max_groups, which makes _calculate_block_metrics take the highest position referenced in a block rather than the number of references. m4 declares no such helper, so it falls through to the generic counter:

    m4_define(probe_test, [AT_SETUP($1) AT_CHECK($1)])   # arity 1; avg_func_args sees 2

    That is a per-function arity error, not a morphology question — $1 … $1 is one parameter referenced twice under any reading, including the ledger's. Two macros in the rosetta shells hit it, and language-crucible/data/m4 reads 70 args against 39 func_start.

    Revised scope: give m4 a capture group and _args_findall_max_groups, mirroring shell. Leave the file-level count alone — it stays ledgered as intended morphology, and keyword-rosetta/data/m4 needs no re-bless.

    Retitled accordingly. The 'Corpus pairing' section of the original description no longer applies.

  3. changed the title [-]args counts `$1` REFERENCES in m4 (+38%), not declared positions: m4 lacks shell's _args_findall_max_groups[/-] [+]m4 avg_func_args counts `$1` references, not declared positions: m4 lacks shell's _args_findall_max_groups[/+] on Sep 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugUnintended behavior or logic failure in the enginecore-engineModifications to the central physics and parsing enginemetricsHeuristics, risk exposures, and topological math updates

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions