Skip to content

Component contracts: constraints on a parameter's value (one-of, type) #31

Description

@fcapolini

Version: @markout-lang/core 0.3.0

A <:define> can give a parameter a default, and that is the whole of what it
can say about it. It cannot say a parameter is required, that it accepts
one of a set, or that its parameter list is closed. So a usage site has
several ways to be wrong that all compile clean and render something.

Repro

<:define tag="ui-btn:a"
  :variant=${'primary'}      // primary | outline | ghost -- a comment, not a rule
  :label=${''}               // required in practice, no sensible default
  class=${`btn btn-${variant}`}
><:slot /></:define>

<ui-btn id="ok"      :variant="outline">Fine</ui-btn>
<ui-btn id="typo-v"  :variant="primry">Misspelled value</ui-btn>
<ui-btn id="typo-n"  :varaint="outline">Misspelled parameter name</ui-btn>
<ui-btn id="missing">No label passed</ui-btn>
<ui-btn id="wrongty" :variant=${42}>Wrong type</ui-btn>

Actual

Compiles clean, five pages of nothing said:

id rendered class what went wrong
ok btn btn-outline
typo-v btn btn-primry a class with no rule; button renders unstyled
typo-n btn btn-primary asked for outline, got primary
missing btn btn-primary no label, and nothing wanted one
wrongty btn btn-42 a number where a variant name goes

typo-n is the worst of them: the author wrote a value, the page ignored it,
and the element silently took its default.

What narrows it

  • :varaint is not ignored — it creates a value. With :aka="b" on the
    usage site, ${b.varaint} reads outline. So a usage site adding a value is
    a real capability and unknown attributes cannot simply be rejected; what is
    missing is a way for a definition to say its list is closed.
  • This is the compiler's own standard, unmet. An unknown reference is a
    compile error anywhere it is written, handler bodies included. An unknown
    parameter at a usage site is accepted in silence. Same class of mistake,
    opposite treatment.
  • It fits silent-failures.md's admission rule exactly — a correct-looking
    page produces wrong output and nothing anywhere says why — but it sits at the
    component-contract level rather than between two framework features, which
    may be why it has not surfaced there.

Why this language is unusually well placed to fix it

Reactive values are declared in markout rather than in JavaScript, so the
compiler already owns every declaration and already resolves every name — the
same property that lets it catch Unknown reference: "cuont" inside a handler
body. Constraining a parameter is an extension of machinery that exists, not a
type layer bolted alongside. A JS-hosted framework needs TypeScript and a build
story to get the same thing.

Possible spellings, not a proposal

Recorded only so the trade-offs are visible; the shape is yours to choose.

  • Required: a parameter declared with no default is required, and omitting
    it is an error. Cheap, but it changes what :x alone means today.
  • A set: :variant=${'primary'} :variant-oneof=${['primary','outline','ghost']},
    checked when the usage site passes a literal and skipped when it passes an
    expression. Catches typo-v and wrongty, and only where it can be sure.
  • Closed list: an opt-in on the definition — every :name at a usage site
    must be a declared parameter. Catches typo-n, which is the one that changes
    what renders. Opt-in because adding values at a usage site is deliberate
    elsewhere.

The three are independent and the third is the one with the highest ratio of
bugs caught to syntax added.

Activity

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

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions