Skip to content

dsc#101: receiver methods bind type parameters on the generic receiver - #105

Merged
samifouad merged 1 commit into
mainfrom
fix/generic-receiver-methods-101
Sep 10, 2026
Merged

dsc#101: receiver methods bind type parameters on the generic receiver#105
samifouad merged 1 commit into
mainfrom
fix/generic-receiver-methods-101

Conversation

@samifouad

Copy link
Copy Markdown
Member

Closes #101. Tracked under #66. Unblocks deka#771 (the ui/* port), which is blocked on Signal<T> having methods and nothing else.

The rule (now stated in the amended RFD 56)

T is bound by the receiver, not declared on the method:

fn (s Signal<T>) get() T
fn (s mut Signal<T>) set(next: T) void

T is introduced by the receiver type and resolves to the receiver value's type argument at each call site. fn (s Signal) get<T>() T is wrong — it reads as if the method could pick a T unrelated to the one the receiver holds. The phase-1 positional spelling stays working (it binds positionally at call sites and generic_struct_signal pins it), but declaring parameters on both the receiver and the method is now an error.

What changed

  • Parser: the receiver position reads an optional type-parameter list using the declaration grammar, so a parameter may carry a bound — fn (x Holder<T: Named>) name() string, union bounds included. Mutating receivers bind the same way.
  • Checker: collection validates the receiver is actually generic, the arity matches the struct declaration, and parameters aren't declared twice. Receiver-bound parameters scope the signature and body, so T resolves like any other type parameter — including the phase-1 capability rule: no member calls on an unbounded T. Bounds declared on the struct's own parameters are inherited where the receiver leaves them unbounded (struct Holder<T: Named> + fn (x Holder<T>) still sees T: Named).
  • Call sites: the receiver's type arguments bind by name and substitute into the signature (signal(42).get()int, signal("x").get()string). A receiver-declared bound is verified against the type argument, since construction only enforces bounds the struct itself declares. substitute_type now also substitutes a Named matching a parameter name, so a receiver method's T can't survive a cross-module call as an opaque named type.
  • Emission unchanged: the type parameter never reaches output, pinned by a transpile test asserting the generic program and its erased equivalent emit byte-identical JavaScript.

Negative fixtures (the ones that matter most)

  • signal(0).set("nope")expected argument type \number`, found type `string`` with a span.
  • A member call on an unbounded receiver T → the normative rfd#56 capability error.
  • A bounded receiver called on a value whose type argument violates the bound → type argument \NoName` ... does not satisfy the bound `Named``, at the call site.
  • Declaration errors: parameters on both receiver and method; arity mismatch (Signal<T, U> on struct Signal<T>); type arguments on a non-generic receiver.

Also decided: construction-site type arguments are rejected, not supported

Signal<T> { value: initial } used to parse </> as comparison operators and report unknown identifier T plus cannot compare types. The parser now recognizes the spelling and rejects it with the design rule: type arguments are inferred from the field values, write Signal { ... } (rfd#56). Supporting the explicit form would add syntax for no gain — inference already solves every construction the corpus contains.

Verification

cargo test --workspace: all 34 suites green (288 deka_syntax unit tests including 9 new ones, cli integration including the emission pin).

Testsuite against the pinned deka 0.45.0 runtime:

Passed: 716 | Failed: 0 | Known: 2 | Skipped: 154 | Total: 872

Six new fixtures under tests/testsuite/generics/, mirroring the phase 1/2 conventions.

-kimi

A receiver method on a generic type can now declare its type parameters
in the receiver position, per the rfd#56 amendment:

  fn (s Signal<T>) get() T
  fn (s mut Signal<T>) set(next: T) void

T is introduced by the receiver and resolves to the receiver value's
type argument at each call site: signal(42).get() yields int and
signal("x").get() yields string. Declaring the parameter on the method
instead (fn (s Signal) get<T>()) stays parseable — it is the phase-1
spelling, binding positionally — but declaring both is an error, since
the parameter is bound by the receiver, not the method.

- Parser: the receiver type reads an optional type-parameter list with
  the declaration grammar, so a parameter may carry a bound:
  fn (x Holder<T: Named>) name() string. Mutating receivers bind the
  same way: fn (s mut Signal<T>) set(next: T) void.
- Checker: collection validates the receiver is generic, the arity
  matches the struct declaration, and the parameters are not also
  declared on the method. Receiver-bound parameters are pushed around
  signature resolution and body checking, so T in the body and
  signature resolves like any other type parameter and obeys the
  unbounded-parameter capability rule (no member calls on an
  unbounded T). Bounds declared on the struct's own parameters are
  inherited where the receiver leaves them unbounded.
- Call sites: the receiver value's type arguments bind the receiver's
  parameters by name and substitute into the signature, and a bound
  declared on the receiver is verified against the type argument —
  construction does not enforce bounds the struct itself does not
  declare, so the method call site checks the contract (rfd#56
  phase 2). substitute_type now also substitutes a Named whose name
  matches a parameter, so a receiver method's T does not survive a
  cross-module call as an opaque named type.
- Emission is unchanged — the type parameter never reaches output,
  pinned by a transpile test asserting the generic and erased forms
  emit byte-identical JavaScript.

Negative fixtures, per the phases 1/2 convention: wrong argument type
at a receiver-method call, a member call on an unbounded receiver T,
a call-site bound violation, and declaration errors (parameters on both
receiver and method, arity mismatch, type arguments on a non-generic
receiver).

Also decided here: explicit type arguments at a construction site
(Signal<T> { value: initial }) are rejected, not supported. The
spelling used to parse < and > as comparison operators and report
unknown identifier T plus cannot compare types; the parser now
recognizes the shape and reports that type arguments are inferred from
the field values, pointing at the inference-only design (rfd#56).

Closes #101. Tracked under #66.
@samifouad
samifouad merged commit 99a9588 into main Sep 10, 2026
4 checks passed
@samifouad
samifouad deleted the fix/generic-receiver-methods-101 branch September 10, 2026 07:17
@samifouad samifouad mentioned this pull request Sep 10, 2026
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.

Generic receiver methods do not parse: fn (s Signal<T>) get() T — blocks the Signal API

1 participant