The coercion turbofish's follow rule #231

Manually merged
hkowalski merged 1 commit from feat/231-the-coercion-turbofish-s-follow-rule into master 2026-09-16 13:06:40 +02:00
Owner

The coercion turbofish's follow rule

Along the way:

  • parser: the coercion turbofish could not carry its own type arguments

Touches 19 files: docs/03-types.md, docs/README.md, docs/examples/bounded_coercion_is_written/Main.ck, docs/examples/bounded_coercion_needs_grouping/Main.ck, docs/examples/bounded_coercion_needs_grouping/expect, docs/examples/bounded_coercion_needs_grouping/project.ck and 13 more.

The coercion turbofish's follow rule Along the way: - parser: the coercion turbofish could not carry its own type arguments Touches 19 files: `docs/03-types.md`, `docs/README.md`, `docs/examples/bounded_coercion_is_written/Main.ck`, `docs/examples/bounded_coercion_needs_grouping/Main.ck`, `docs/examples/bounded_coercion_needs_grouping/expect`, `docs/examples/bounded_coercion_needs_grouping/project.ck` and 13 more.
hkowalski approved these changes 2026-09-16 11:08:38 +02:00
hkowalski left a comment

Approving. I would have split src/rust/crates/ck_parser/src/path.rs differently, but not enough to hold this up.

Approving. I would have split `src/rust/crates/ck_parser/src/path.rs` differently, but not enough to hold this up.
@ -140,3 +229,3 @@
| SyntaxKind::Or => {}
// Anything else — not a generic-argument list: it is a comparison.
_ => return false,
_ => return None,
Owner

The catch-all arm means a variant added later compiles and lands here without a word. Can the match on _ name every variant so the compiler tells us?, at src/rust/crates/ck_parser/src/path.rs:231.

The catch-all arm means a variant added later compiles and lands here without a word. Can the match on `_` name every variant so the compiler tells us?, at src/rust/crates/ck_parser/src/path.rs:231.
Author
Owner

Named them all. A new variant is a compile error in src/rust/crates/ck_parser/src/path.rs now, which is where we want to find out.

Named them all. A new variant is a compile error in `src/rust/crates/ck_parser/src/path.rs` now, which is where we want to find out.
mdoorn approved these changes 2026-09-16 12:30:21 +02:00
mdoorn left a comment

Second read on src/rust/crates/ck_parser/src/path.rs. Nothing to add.

Second read on `src/rust/crates/ck_parser/src/path.rs`. Nothing to add.
`x::Name<…>` is the §1b coercion surface and the arguments are part of it —
"it starts with a letter and names the WHOLE type". Every receiver could carry
them except the one people write: on a bare identifier, `a::String<128>` was
E0146 / E0048 / E0050 / E0036, because `::Name` is uniform with `Mod::member`
and a path segment has nowhere to put type arguments, so the `<` went back to
the expression parser as less-than. Bare `String` covered most uses precisely
because an alias carries its bound, which is why the gap bit hardest where no
alias exists: every `List<T, N>`, every `String<N>` off 64.

The cell recording it argued this was an owner call about the grammar and not a
mechanical fix, and it was right: the one-line relaxation is measurably wrong.
`parse_bare_angle_generic_args` already admits a bare-angle list on a segment —
that is how `Add<f32, f32, f32>::add` parses — gated on the matching `>` being
followed by `::`. Relaxing that to admit a TERMINAL list eats

    two(a < b, c > d)      // two comparisons in one argument list

whose seven tokens are every one of them tokens a type-argument list may
contain. That is a miscompile of a comparison, traded for a pair of
parentheses.

THE RULE, AND WHY IT IS EXACT RATHER THAN CAREFUL

A terminal `<…>` on a `::`-reached segment is a coercion target iff the token
after the matching `>` cannot BEGIN an expression:

    `)`  `}`  `]`  `|)`  `,`  `;`  a declaration keyword  end-of-file

The proof is one sentence and it is not a heuristic with headroom: a real `>` is
the binary comparison operator, so it always takes a right operand, so it is
always followed by a token that STARTS an expression. The two sets are disjoint
by construction — no comparison can be read as a type-argument list, and no
coercion standing in one of those positions can be read as a comparison.

`(`, `.`, `[` and `{` are deliberately OUT: they continue or open an expression,
so a coercion the reader means to call into keeps its grouping. The declaration
keywords come from `is_top_level_decl_start` — the same list top-level recovery
syncs on, so a new keyword joins the set by existing — with exactly ONE
subtraction, which is what makes the disjointness claim literally true instead
of nearly true: `trait` is also a VALUE head (`trait.method(…)`, the
operator-witness inside an `operator` body), so it terminates only when no `.`
follows. Two further narrowings: never the HEAD segment (which puts the program
above out of reach of the rule entirely) and never in PATTERN position, where
there is no value to coerce.

It is DECLARED before it is implemented. `Grammar.ckx` gains `coercion_tail`
(`'::' <target: type_atom>`) beside `turbofish`, carrying the follow set and the
disjointness argument; the parser reads the rule off that page rather than
inventing a second copy of it.

NOTHING DOWNSTREAM MOVED, which is the other half of why this shape was chosen.
The parser DECLINES the segment and leaves the whole `::` tail for the postfix
loop, so `a::String<128>` builds the exact `CoercionExpr` that
`(a)::String<128>` already built — `ck_types::value_turbofish::synth_coercion`,
untouched. `b.s::T<n>`, `xs[i]::T<n>` and `f()::T<n>` were already reaching that
fold and are unchanged.

CELLS. `coercion_turbofish_with_type_arguments_needs_parentheses` is fixed and
renamed to `..._on_an_identifier` (`runs: 42`); its prose no longer argues a
fork, it records how the fork was closed. `list_coercion_turbofish_widens_a_local`
drops its parentheses and its "THE PARENTHESES ARE NOT STYLE" paragraph. Three
new cells pin the rule from all three sides:
`two_comparisons_in_an_argument_list_stay_comparisons` (the counter-example,
still `runs: 42`), `coercion_turbofish_ends_an_expression_bodied_fn` (the two
follow members that are not punctuation — the next declaration and end-of-file —
on one page), and `coercion_turbofish_under_a_method_call_needs_parentheses`
(the set's closed OTHER side: `.` is out, and the day that ruling changes this
cell says so). CELL_COUNT 454 -> 457, BROKEN_CELL_COUNT 47 -> 46.

Two stale claims went with it, both live and both now false: `ck_types`'s
`coercion_target_segment` said "the parser cannot carry generic arguments on a
path segment", and `rebound_hint`'s doc said the parentheses it prints are
mandatory because `x::String<64>` does not parse. It does now. The message keeps
the parentheses — `value` stands for whatever the author wrote, and an operator
expression or a continued coercion still needs them — but for the true reason.

The book follows: ch.3's bounded-coercion section loses its parentheses,
`bounded_coercion_is_written` grows the four bare receivers (name, field, index,
call) plus the grouped spelling, and `bounded_coercion_needs_grouping` is a new
fixture pinning that `.` is outside the follow set. 37 -> 38 expectations.

Gates: 4 legs green (1371 unit, 106 corpus apps, 457 cells, 38 book
expectations); test-integration ALL GREEN — 251 passed, 1 ignored, and the
allow-listed `r3_no_file_exceeds_loc_cap` standing red unchanged. fmt clean,
clippy clean. No conformance golden moved: the change adds a spelling and
removes none.
hkowalski manually merged commit 94054f6cc9 into master 2026-09-16 13:06:40 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
northgale/cklang-bootstrap!231
No description provided.