Invisible link to canonical for Microformats

ADR-027 Support type tags on struct fields (including multiple tags and @struct(…) specialization)


Status

Accepted

Context

Struct fields previously supported a single “type hint” (effectively one tag) that could be used to validate values at runtime. This created a few limitations:

  • Only one constraint could be expressed per field (e.g., you could say “this is a @str” but not “this is a @str and also something else”).
  • Structs themselves were not consistently expressible as a first-class type tag in the same way other tagged types are.
  • There was no clean way to express “this field must be a struct of this specific schema”.

As the language adds richer typing/tagging capabilities (and as codebases start to rely on more precise struct schemas), we want struct fields to support multiple tags and introduce a schema-aware struct tag.

Decision

1) Replace single struct field hint with a list of tags

Struct field typing metadata changes from a single string hint to a list of tags:

  • AST struct fields now carry Tags (a list) rather than a single Hint.
  • Struct schema fields store the same list of tags.

This enables multiple type constraints to be declared on a field (e.g., @str @fn fieldName, etc.), and keeps the representation aligned with the general “tag” concept.

2) Parse and preserve multiple tags on struct fields

The parser now collects all consecutive tags that appear before a struct field name, rather than only accepting one.

This allows field declarations like:

  • @str name
  • @str @bool flags (illustrative example of multiple constraints)

3) Add @struct as a supported type tag (with optional schema specialization)

We introduce special handling and validation for struct typing:

  • @struct matches:
    • struct values, and
    • struct schema definitions (where relevant)
  • @struct(User) additionally requires the struct’s schema name to match User
    • The argument may be provided as an identifier or as a string (e.g., @struct("User")).

This makes it possible to express “this must be a struct” and “this must be a struct of schema X” directly in field tags.

4) Define validation semantics: all tags must match (intersection)

When validating a struct instance against its schema, the field’s tags are treated with intersection semantics:

  • If a field has multiple tags, every tag must match the provided value (unless existing semantics treat nil as permissive in certain matching paths, as described below).

Special cases:

  • @fn matches both function objects and function-group objects (preserving prior behavior).
  • @struct / @struct(User) follows the struct-specific rules above.
  • Other tags use the existing tag-to-type mapping rules.

5) Validate tags at schema construction time

When building a schema, field tags are validated:

  • Unknown tags are rejected.
  • @struct is currently the only type tag that accepts parameters, and it must have at most one argument.

This ensures invalid type hint declarations fail early and with source-positioned errors.

Consequences

Positive

  • More expressive schemas: multiple constraints per field are now possible.
  • First-class struct tagging: @struct is supported like other type tags.
  • Schema-specific struct constraints: @struct(Name) enables stronger validation and clearer intent.
  • Better alignment with the language’s tagging model: tags are consistently represented as tag objects rather than ad-hoc strings.

Negative

  • Breaking internal representation change: anything expecting a single field hint must migrate to Tags.
  • More complex validation logic: multiple tags and special cases add branching and require careful test coverage.
  • Potential confusion around nil: some matching paths treat nil as permissive to preserve existing behavior, which can surprise users expecting strict enforcement.

Neutral

  • Debug/inspection output for struct fields now reports tags rather than a single hint value.
  • Error messages may change slightly (e.g., referencing a tag string vs. a hint string), but remain source-positioned and actionable.