5.9 KiB
G-072 punk::args compound clause types: named alternates with per-element typing and per-alternate arity (try-class handlers)
Status: proposed Scope: src/modules/punk/args-999999.0a1.0.tm (type-expression parsing, clause allocation, synopsis/help renderers), src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm (::try as proving consumer; ::if/::switch as touched), src/tests/modules/punk/args/testsuites/args/ Goal: heterogeneous repeating clauses - try's on/trap handlers are the motivating case - can be modelled with per-alternate element types, choices and arity: 'try {} on bogus {e} {s}' is rejected with the code choice set displayed, alternates may differ in element count, mixed-order interleaving keeps parsing, and synopsis/help render each alternate's shape distinctly instead of a conflated element - with the definition mechanism (named compound types, composition operators, cross-package scoping) decided and recorded, informed among others by set-theoretic type composition. Acceptance: a definition mechanism (decided in the work, starting from the sketches embedded in the ::try moduledoc notes: named compound types with arity, @typeimport, composition operators) lets the ::try handler be modelled so that parse rejects 'on bogus ...' displaying the on-code choices (ok/error/return/break/continue or integer) and types the trap pattern as a list, while mixed-order on/trap interleaving and finally positioning keep parsing (the 2026-07-12 probe matrix stays green); alternates of differing arity are expressible and exercised by at least one fixture (per-alternate arity derived or declared - decision recorded); synopsis and help output render each alternate's element shape distinctly (the deliberately-unpiped oncode_or_trappattern -typesynopsis workaround retired); type-name scoping across documentation packages has a recorded decision (namespacing/@typeimport mechanism, or rejection with rationale); the ::try moduledoc adopts the mechanism with real-vs-model parity probes added (::if/::switch adopt it only where it improves their models); definitions not using compound types behave unchanged (full punk::args suite passes, none weakened).
Context
The ::try moduledoc models handlers as a single 4-element clause {literal(on)|literal(trap) string list string} -multiple 1. Probing (2026-07-12, G-041 prework) confirmed this parses everything structural correctly - mixed on/trap order, finally positioning - but over-accepts semantically: the second element cannot be typed per-alternate, so 'on bogus {e} {s}' passes where Tcl errors with the valid completion codes. The moduledoc carries extensive embedded design notes at ::try recording the difficulty and sketching mechanisms (named compound types with arity indicators such as <on_handler:4>|<other_handler:3>, per-definition vs global type registries, @typeimport for cross-package reuse, RPN composition like {stringstartswith a stringendswith z AND int OR}). Related over-acceptance findings from the same probes are listed in goals/G-055-tclcore-regen-workflow.md Notes (reserved-word clause allocation, '-' fallthrough position constraints) - candidates to fall out of the same mechanism or be recorded as out of scope here.
Set-theoretic types input (user direction 2026-07-12)
Investigate whether gradual set-theoretic types as adopted by Elixir (https://elixir.hexdocs.pm/main/gradual-set-theoretic-types.html) can inform the mechanism. Brief examination at drafting time:
- Elixir treats types as SETS of values composed with union (or), intersection (and) and negation (not); literal values are singleton types; dynamic() effects gradual typing; multi-clause functions are intersections of arrow types.
- Mapping to punk::args: -type's existing | is already set union and literal(word) is already a singleton type - the existing vocabulary is a fragment of a set-theoretic algebra, so extension is consistent rather than a redesign. punk::args' 'any' plays the dynamic() role.
- Negation would directly express the reserved-word gap recorded in G-055's notes: an if else-script is "script and not literal(elseif)" - i.e. deny-lists for clause members become type negation instead of a bespoke feature.
- Intersection covers the composite constraints the ::try notes sketched as RPN: {stringstartswith a stringendswith z AND int OR} reads as ((startswith(a) and endswith(z)) or int) - infix/functional composition on set semantics can replace the RPN idea.
- Named compound types then become type ALIASES over such compositions, and a heterogeneous clause is a UNION of tuple-like clause shapes whose arity derives from each shape's length - answering the notes' open question about declaring arity for alternates of differing length (arity is per-alternate and computed, not separately declared).
- A full algebra is likely overkill for the immediate try case; the design should pick operator spellings consistent with set-theoretic composition so later extension (negation for reserved words, intersection for composite string constraints) slots in without breaking earlier definitions.
Notes
- Related: G-041 (form selection and clause allocation interact; its candidacy machinery should not need changes from this goal's types), G-071 (allocation correctness for optional elements, achieved 2026-07-12 - see goals/archive/G-071-punkargs-optional-allocation.md; its choiceword_match allocation screen and allocation.test fixtures are the base this goal's alternates ride on), G-055 (modelability findings list; its parity workflow verifies whatever this goal makes expressible), G-053 (occurrence arity - adjacent clause machinery).
- Display cost matters: the ::try notes warn bracketed alternate forms "get unwieldy in synopsis listings" - synopsis rendering of compound types is part of the mechanism's acceptance, not an afterthought.
- Archived-goal references in this file: G-041 achieved 2026-07-13 (goals/archive/G-041-punkargs-form-matching.md).