Skip to content

Add intersection ("A & B") and union ("A | B" / "[A | B]") type syntax - #1700

Open
apiology wants to merge 1 commit into
lsegal:mainfrom
apiology:issue-1644-literal-syntax
Open

Add intersection ("A & B") and union ("A | B" / "[A | B]") type syntax#1700
apiology wants to merge 1 commit into
lsegal:mainfrom
apiology:issue-1644-literal-syntax

Conversation

@apiology

@apiology apiology commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds three related type-tag syntax elements:

  • & (intersection, closes Syntax for intersection types #1644): Foo & Bar means a value must
    satisfy both Foo and Bar, matching Solargraph's syntax
    (Add intersection (A & B), union (|), and grouping ([...]) type syntax castwide/solargraph#1231). Legal in every position a type can appear,
    and always binds tighter than the union or slot separator around it,
    matching RBS's documented precedence. Renders as "both a Foo and a
    Bar" (or "all of a Foo, a Bar, and a Baz" for 3+), to avoid reading
    like two separate values.
  • | (union, closes No general grouping syntax for unions nested inside tuples, intersections, etc. #1699): marks a union - a value matching any of
    the listed types. Some type lists already mean a union without it (the
    top level, a hash's key/value lists, [...], and
    Array<...>/Set<...>), so , and | land on the same result there.
    Elsewhere, each comma-separated item is a distinct, positional type
    parameter instead (a fixed-order list like Array(...), or <...> for
    a name other than Array/Set) - there, | groups alternatives
    within a single one of them: Array(Foo | Bar, Baz) is a 2-element
    Array whose first element is a Foo or a Bar, and
    Result<Success | Failure, Other> is a Result whose first type
    parameter is a Success or a Failure.
  • [...] (closes No general grouping syntax for unions nested inside tuples, intersections, etc. #1699): used the same way parentheses are in
    algebra, to override the default order of operations - e.g. to use a
    union as one conjunct of an intersection, which otherwise has no way to
    mark where the union ends: [Foo | Bar] & Baz.

Also documents three pre-existing but previously undocumented anonymous
shorthand forms - <A>, (A), {A=>B} - where the leading type name can
be omitted and defaults to Array/Hash (see #1701), and stops
Foo<A, B> from always being read as a union: <...>'s type parameters
are conventionally used both ways - a homogeneous collection's implicit
union of element type(s) (Array<String, Symbol>), or a class's
distinct, positional type parameters (Result<Success, Failure>).
Array/Set (and any name with a single type parameter) keep the union
reading; Hash<K, V> gets its own dedicated key/value rendering matching
Hash{K=>V}; anything else with 2+ parameters reads neutrally ("a Result
with type parameters (a Success, a Failure)"). This choice is made
entirely at render time (CollectionType#to_s) - the parser always
treats <...> the same way it already treats (...), with no
name-specific knowledge at all.

Full rules and examples are in the new "Operator Precedence" and
"Overriding the Order of Operations" sections of docs/Tags.md, and the
rewritten "Parameterized Types"/"Union Operator" sections.

Test plan

  • bundle exec rspec spec/tags/types_explainer_spec.rb - specs for
    IntersectionType/GroupType/CollectionType#to_s, parser-level
    precedence/error cases, and end-to-end .explain examples.
  • bundle exec rspec - full suite green (2830 examples, 0 failures).

apiology added a commit to apiology/yard that referenced this pull request Jul 31, 2026
Combines intersection type support ("A & B", from lsegal#1700) with grouped-
union support ("[A | B]", from lsegal#1702) into a single PR, per review
feedback, and fixes an integration bug the merge surfaced: the
grouping syntax's "|" handler pushed its type directly instead of
routing through finish_intersection, which would have silently
dropped any pending "&" conjuncts when a group boundary was hit (e.g.
in "[Foo & Bar | Baz]").

Also fixes a real English-rendering ambiguity in the combined output:
GroupType wraps its whole member list in one pair of parens, but
IntersectionType (and a multi-method DuckType, "#foo & #bar") render
as a bare "X and Y" with no punctuation of their own. Sitting next to
a sibling in the group's "or"-joined list, that read ambiguously
("a Foo and a Bar or a Baz" doesn't show which operator binds
tighter) even though the parse itself was correct. GroupType now adds
defensive parens around exactly those two cases.

Documents operator precedence explicitly: "&" always binds tighter
than whichever separator surrounds it ("," at the top level, "|"
inside "[...]"); "|" is only valid inside "[...]"; "," is never valid
inside "[...]". Adds end-to-end specs combining both operators,
including the precedence and disambiguation cases above.
@apiology apiology changed the title Add intersection type ("A & B") support to type tags Add intersection ("A & B") and union ("A | B" / "[A | B]") type syntax Jul 31, 2026
Comment thread docs/Tags.md Outdated
Comment thread lib/yard/tags/types_explainer.rb Outdated
Comment thread docs/Tags.md Outdated
Comment thread docs/Tags.md
Comment thread docs/Tags.md Outdated
Comment thread docs/Tags.md Outdated
Comment thread lib/yard/tags/types_explainer.rb
Comment thread docs/Tags.md Outdated
Comment thread docs/Tags.md Outdated
Comment thread docs/Tags.md Outdated
Comment thread docs/Tags.md Outdated
Comment thread docs/Tags.md Outdated
Comment thread docs/Tags.md Outdated
Comment thread docs/Tags.md Outdated
Comment thread docs/Tags.md Outdated
Comment thread docs/Tags.md Outdated
Comment thread docs/Tags.md Outdated
Comment thread docs/Tags.md Outdated
Comment thread docs/Tags.md Outdated
Comment thread docs/Tags.md Outdated
Comment thread docs/Tags.md Outdated
Comment thread docs/Tags.md Outdated
Comment thread lib/yard/tags/types_explainer.rb Outdated
…e override

Adds three related type-tag syntax elements:

- `&` (intersection, closes lsegal#1644): `Foo & Bar` means a value must
  satisfy both `Foo` and `Bar`, matching Solargraph's syntax
  (castwide/solargraph#1231). Legal in every position a type can appear,
  and always binds tighter than the union or slot separator around it,
  matching RBS's documented precedence. Renders as "both a Foo and a
  Bar" (or "all of a Foo, a Bar, and a Baz" for 3+), to avoid reading
  like two separate values.
- `|` (union, closes lsegal#1699): marks a union - a value matching any of the
  listed types. Some type lists already mean a union without it (the top
  level, a hash's key/value lists, `[...]`, and `Array<...>`/`Set<...>`),
  so `,` and `|` land on the same result there. Elsewhere, each
  comma-separated item is a distinct, positional type parameter instead
  (a fixed-order list like `Array(...)`, or `<...>` for a name other than
  `Array`/`Set`) - there, `|` groups alternatives within a single one of
  them: `Array(Foo | Bar, Baz)` is a 2-element Array whose first element
  is a Foo or a Bar, and `Result<Success | Failure, Other>` is a Result
  whose first type parameter is a Success or a Failure.
- `[...]` (closes lsegal#1699): used the same way parentheses are in algebra,
  to override the default order of operations - e.g. to use a union as
  one conjunct of an intersection, which otherwise has no way to mark
  where the union ends: `[Foo | Bar] & Baz`.

Also documents three pre-existing but previously undocumented anonymous
shorthand forms - `<A>`, `(A)`, `{A=>B}` - where the leading type name can
be omitted and defaults to `Array`/`Hash` (see lsegal#1701), and stops
`Foo<A, B>` from always being read as a union: `<...>`'s type parameters
are conventionally used both ways - a homogeneous collection's implicit
union of element type(s) (`Array<String, Symbol>`), or a class's
distinct, positional type parameters (`Result<Success, Failure>`).
`Array`/`Set` (and any name with a single type parameter) keep the union
reading; `Hash<K, V>` gets its own dedicated key/value rendering matching
`Hash{K=>V}`; anything else with 2+ parameters reads neutrally ("a Result
with type parameters (a Success, a Failure)"). This choice is made
entirely by `CollectionType#to_s` at render time - the parser always
treats `<...>` the same way it already treats `(...)` (`,` separates
positional type parameters, `|` groups alternatives within one of them),
with no name-specific knowledge at all.

Full rules and examples are in the new "Operator Precedence" and
"Overriding the Order of Operations" sections of `docs/Tags.md`, and the
rewritten "Parameterized Types"/"Union Operator" sections.

Test plan:
- `bundle exec rspec spec/tags/types_explainer_spec.rb` - specs for
  `IntersectionType`/`GroupType`/`CollectionType#to_s`, parser-level
  precedence/error cases, and end-to-end `.explain` examples.
- `bundle exec rspec` - full suite green (2830 examples, 0 failures).
@apiology
apiology force-pushed the issue-1644-literal-syntax branch from d78cc24 to 6521988 Compare August 1, 2026 20:37
@apiology
apiology marked this pull request as ready for review August 1, 2026 21:03
@apiology

apiology commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

@lsegal this one is ready for review

apiology added a commit to apiology/solargraph that referenced this pull request Aug 1, 2026
lsegal/yard#1700 proposes standardizing `|` as an explicit union
operator and `[...]` as a grouping construct for YARD type tags,
alongside the `&` intersection operator this branch already added for
solargraph#1229. Implementing the full syntax here so Solargraph's own
parser and the upstream proposal describe the same grammar, and so
`(A | B) & C` - previously only buildable by translating real RBS or
constructing an Intersection object directly, per the now-outdated
comment on the parentheses spec - has an actual tag-string form.

`|` binds looser than `&` (matching RBS's documented precedence) and,
inside a fixed-arity context (`Array(...)` tuples, or a generic type's
positional parameters), groups multiple types into a single slot
instead of splitting into separate positional arguments - the same
distinction `,` already makes there. In an implicit-union context
(Array<...>/Set<...>, hash key/value lists, the top-level list itself),
`|` and `,` land on the same result, since every comma-separated type
in those contexts is already unioned regardless of grouping.

`[...]` is the actual grouping construct - the only way to mark where
a union ends when it needs to be one conjunct of an intersection
(`[Foo | Bar] & Baz`). It's deliberately conservative about when it
opens: only at a fresh atom (blank base, not already nested in
<>/{}/()), otherwise `[`/`]` are ordinary characters - this matters for
quoted string-literal types like `"[]"`, which have no concept of
grouping and would otherwise crash self-typecheck against the real Dir
RBS core stub.

Also fixes the anonymous shorthand forms `<A>`, `(A)`, `{A=>B}` (typed
before this as an empty-name UniqueType) to default their name to
Array/Array/Hash respectively, per YARD #1700's third documented
change - so an anonymous form now behaves exactly like its named
equivalent, including for rooting.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wfsRatLaRbxcsQ7ZVzifN
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No general grouping syntax for unions nested inside tuples, intersections, etc. Syntax for intersection types

1 participant