You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

12 KiB

G-074 punk::args multiform ambiguity analysis: on-demand form-overlap detection with sanctioned-overlap annotation

Status: achieved 2026-07-13 Scope: src/modules/punk/args-999999.0a1.0.tm (analysis command, @form sanction key as decided in the work), src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm (::after/::lseq as proving consumers, sanction adoption), src/tests/modules/punk/args/testsuites/args/ (new suite) Goal: an on-demand analysis command (candidate spelling punk::args::formcheck ) reports, for a multiform definition, the form pairs an argument list could cleanly match simultaneously - a conservative static pass over the forms' leading slots and arity windows that flags discriminator-vs-permissive-type alignments (the 'lseq 1 count 5' class, where a literal/choice discriminator in one form aligns with a non-validating type like expr/any/string in another) and discriminator-free overlaps (the 'after cancel id|script' class) - with a definition-level annotation (spelling decided in the work, e.g @form key) that sanctions known/documented overlaps so they report as acknowledged rather than as findings, keeping the unsanctioned report actionable; the define hot path is untouched (analysis runs only on demand - interactive, test-time, and as a G-055 verification-gate step for regenerated multiform commands). Acceptance: the analysis command reports the two known real cases from the G-041 closeout - ::lseq range/start_count overlap via the expr-typed end slot (unsanctioned finding: type-weakness class) and ::after cancelid/cancelscript (documented-overlap class) - and reports nothing for multiform definitions whose forms are fully discriminated (the parse withid/withdef pair, the forms.test afterish fixture); classifications distinguish at minimum type-weakness overlap (a discriminator aligned with a permissive type) from structural overlap (no discriminating slot exists); the sanction annotation silences (or downgrades to acknowledged) a listed form pair without affecting parse behaviour, is rejected at definition resolve when it names unknown forms, and is adopted for the after cancel pair in the tclcore moduledoc; the analysis is conservative in the documented direction (may miss deep ambiguities, must not false-alarm on discriminated forms - the miss/report boundary is documented with the slot model); running it performs no parse of user-supplied words and adds no work to define/resolve for definitions that never call it; a new testsuite covers the finding classes, the sanction, the unknown-form rejection and the no-finding cases; full punk::args suite passes.

Context

User direction 2026-07-13 (during the G-041 closeout): "The encountered ambiguities suggest weakness in the doc-blocked types - and perhaps requirement to evaluate for ambiguities at define time to reject / warn"; the lint-time (on-demand) shape of this goal was recommended and user-selected ("draft lint-time analysis").

The two ambiguities G-041's candidacy machinery surfaced on the tclcore models:

  • 'lseq 1 count 5' -> multipleformmatches range+start_count. Root cause is a TYPE-WEAKNESS overlap: start_count's literalprefix(count) discriminator aligns positionally with range's end slot typed number|expr, and -type expr performs no validation, so the word 'count' satisfies both forms. Real lseq resolves it semantically (start_count). The G-055 detail file carries the operand-typing probes (2026-07-13): indexexpression is a poor lexical fit (under-accepts 1e2/2*3/sqrt(9)/bracketed exprs, over-accepts end/end-1); the precise discriminator is expr SYNTAX validation, needing a non-evaluating parser (G-069/G-070 territory); TIP 746 makes the operands number-only on 9.1+.
  • 'after cancel someid' -> multipleformmatches cancelid+cancelscript. A GENUINE documented overlap: 'after cancel id' vs 'after cancel script ?script?...' are textually indistinguishable for one trailing word - real Tcl disambiguates by the id's runtime shape. Correct in a doc-faithful model, but indistinguishable from an authoring mistake without a sanction mechanism.

Full ambiguity detection is language intersection between forms - not decidable in general (types like regex-validated strings, unbounded -multiple tails) - but the encountered classes are cheap to detect statically: walk the aligned leading slots of each form pair over their overlapping total-arity windows and test whether every slot pair is co-satisfiable, flagging pairs where a discriminator slot (literal/literalprefix/restricted-choices) faces a permissive type (expr, any, string, none-validating) and pairs with no discriminating slot at all.

Approach

  • Pairwise form analysis over the resolved spec (FORMS/ARG_INFO - no re-parse of definitions): compute each form's value-arity window (VAL_MIN/VAL_MAX plus optional-member clause arithmetic as in the allocator); for pairs with intersecting windows, align leading slots and classify each aligned pair as disjoint (two different literals; literal vs type that rejects it), co-satisfiable-weak (discriminator vs permissive type), or co-satisfiable-equal (same/overlapping permissive types). A pair with no disjoint slot in any aligned position within the shared window is a finding: type-weakness if some slot is discriminator-vs-permissive, structural otherwise.
  • Choice/literal words screened via choiceword_match (the shared G-040 resolver) so the static disjointness test cannot diverge from parse acceptance - the same principle that kept G-071's allocation screen and G-051's doc walk honest.
  • Options: forms whose option sets differ do not discriminate positionally (options are order-free); first pass ignores opts for alignment (documented as part of the conservative boundary) - the arity windows still gate.
  • Sanction: an @form-level key (spelling decided in the work; candidates -overlapallowed on either member, or a definition-level directive) recorded in the spec, validated at resolve (unknown form names error), consumed only by the analysis (parse behaviour unchanged - G-041's multipleformmatches error still fires; the sanction documents intent, it does not pick a winner).
  • Report shape: machine-parsable dict (per finding: forms, class, witness slot(s), an example witness arglist shape where derivable, sanctioned flag) plus a human summary - the dict is what the G-055 verification gate and test pins consume.
  • Consumers: G-055's regeneration workflow gains a gate step "run formcheck on every regenerated multiform command; new unsanctioned findings block acceptance"; interactive use for definition authors debugging multipleformmatches errors.

Notes

  • Related: G-041 achieved 2026-07-13 (goals/archive/G-041-punkargs-form-matching.md)
    • its candidacy machinery is the runtime oracle for a GIVEN arglist; this goal is the static answer for ALL arglists, and its Progress section records both motivating ambiguities. G-055 (verification-gate consumer; operand-typing probes in its notes), G-072 (compound clause types change the slot model - if active concurrently, coordinate the alignment walk), G-069/G-070 (a future expr syntax-validating type would convert the lseq finding from type-weakness to resolved - the analysis should then report the pair as discriminated).
  • Define-time always-on checking was considered and rejected with the user's concurrence: define/resolve is a hot path and most definitions are single-form; on-demand covers the authoring and regeneration moments where findings are actionable. A future opt-in (@dev-time strict mode) could call the same analysis.
  • The 'after cancel' sanction is also the display question's anchor: a sanctioned overlap is why the G-041 doc surface marks BOTH forms and shows the ambiguity message - the sanction must not suppress that runtime honesty.

Progress

2026-07-13 (agent): implemented whole in punk::args 0.12.0 + tclcore moduledoc 0.3.4.

Command spelling: punk::args::formcheck ?-return dict|summary? (default dict; the dict carries the summary text as a key, so one return shape serves both the G-055 gate and interactive use). Sanction spelling: @form -overlapallowed on either pair member (stored in the FORMS dict by the existing @form key-merge - no record-parsing change); unknown form names rejected in resolve's end-of-forms cycle (all forms exist by then).

Design refinement over the drafted pure-static pass, adopted during implementation: the static analysis PROPOSES, a real parse CONFIRMS. The pairwise pass enumerates each form's positional word-slot chains (private::formcheck_chains - leaders then values, branching on -optional args and ?-wrapped clause members, -multiple capped at 2 repetitions), screens aligned equal-length chains per position (formcheck_slotinfo/formcheck_slot_accepts - discriminator words via choiceword_match exactly as drafted, type witnesses from a small table, unknown types optimistically 'maybe'), and then parses each candidate witness arglist against both forms individually (parse_status -form - no raise, no user-supplied words). Only a witness that parses cleanly against BOTH forms is reported, which upgrades the no-false-alarm requirement from 'conservative heuristic' to 'true by construction' - and the confirmed witness is exactly the report's example arglist. The miss/report boundary (documented in the formcheck help): witness derivation can fail for types without a derivable word, forms requiring options (options excluded from the positional model as drafted), and enumeration caps.

Findings on the motivating models:

  • ::lseq: range/start_count type_weakness, witness {1 count 1} (range's number|expr end slot swallows the literalprefix(count) word) - PLUS a second real finding the static draft didn't predict: range/count type_weakness, witness {1 by 1} (same expr-typed end slot swallows count-form's 'by' discriminator). Both genuinely raise multipleformmatches; both left unsanctioned (actionable - resolved when an expr syntax-validating type arrives, G-069/G-070).
  • ::after: exactly one finding, cancelid/cancelscript structural, witness {cancel after#}; sanctioned via -overlapallowed on the cancelid @form line (tclcore 0.3.4) leaving zero unsanctioned findings. Parse honesty pinned: multipleformmatches still raises for id-shaped cancel words.
  • No findings: ::punk::args::parse (withid/withdef literals), the afterish fixture (int vs literal leading slots), the sharedform fixture (arity windows).

Tests: new args/formcheck.test (7 tests) - no-finding cases, class + sanction on a local fixture, unknown-form resolve rejection, -return summary, ::lseq/::after pins (witnesses re-verified against both forms inside the test), sanction parse-neutrality. Full punk::args suite 210 passed / 0 failed; punk::ns suite 57/57.

Acceptance review: two known G-041 cases reported with the required classes - yes (::lseq range/start_count type-weakness unsanctioned; ::after cancelid/cancelscript documented-overlap sanctioned). Nothing reported for discriminated multiform definitions - yes (parse pair + fixtures pinned). Classifications distinguish type-weakness from structural - yes (relation recorded per witness slot). Sanction downgrades to acknowledged without parse effect, rejects unknown forms at resolve, adopted for after cancel - yes (all pinned). Conservative in the documented direction - yes, strengthened: findings are witness-confirmed so false alarms are impossible by construction; misses documented with the slot model in the command help. No parse of user-supplied words (witnesses are synthetic), no define/resolve cost for definitions that never call it (only the O(list) -overlapallowed name check when the key is present) - yes. New testsuite covering finding classes/sanction/rejection/ no-finding - yes. Full suite passes - yes.

Deferred (recorded, not blocking): G-055's workflow doc gains the formcheck gate step when G-055 activates (noted in its detail file); punk::ns::cmdhelp does not yet surface sanctioned-overlap acknowledgement in rendered help (the G-041 both-forms marking already shows the ambiguity - display-side integration can ride a future doc-surface goal).