Skip to content

Fix #481: subtype check for refinement type members uses semantic equality#594

Open
MilosM348 wants to merge 2 commits into
zio:developfrom
MilosM348:fix/refined-type-subtype
Open

Fix #481: subtype check for refinement type members uses semantic equality#594
MilosM348 wants to merge 2 commits into
zio:developfrom
MilosM348:fix/refined-type-subtype

Conversation

@MilosM348

Copy link
Copy Markdown

/claim #481

What

Fixes the long-standing #481 regression: a trait that fixes an inherited abstract
type member should be a subtype of the structural refinement form fixing the
same member, but Tag[AInt].tag <:< Tag[A { type T = Int }].tag was returning
false.

Why

LightTypeTagInheritance.compareDecl compared two RefinementDecl.TypeMember
declarations with lref == rref — strict structural AST equality. Two refs can
denote the same type while differing in incidental shape (presence/absence of an
empty Boundaries.Empty, distinct prefix chains for the same symbol, refinement
form vs. plain NameReference for a fixed type member, etc.), so the ==
short-circuited to false and a perfectly valid subtype relation was lost.

How

Mutual subtyping is the principled invariant-equality check: two refs denote the
same type iff each is a subtype of the other. Keep the == fast-path so the
common case stays cheap.

case (RefinementDecl.TypeMember(ln, lref), RefinementDecl.TypeMember(rn, rref)) =>
  ln == rn && (lref == rref || (ctx.isChild(lref, rref) && ctx.isChild(rref, lref)))

…equality

Reproduction (zio#481):

  trait A { type T }
  trait AInt extends A { override type T = Int }

  Tag[AInt].tag <:< Tag[A { type T = Int }].tag // returned `false`, should be `true`

The bug lives in `LightTypeTagInheritance.compareDecl` for the case where both
sides have a `RefinementDecl.TypeMember` and the right-hand-side member is
*concrete* (e.g. `type T = Int`), not abstract (e.g. `type T <: AnyVal`).
Type-member assignments are invariant, so the historical implementation used
structural AST equality:

  case (RefinementDecl.TypeMember(ln, lref), RefinementDecl.TypeMember(rn, rref)) =>
    ln == rn && lref == rref

Two `LightTypeTagRef` AST values can denote the *same* type while differing in
incidental shape — presence/absence of an empty `Boundaries.Empty`, distinct
prefix chains for the same symbol, refinement form vs. plain `NameReference`
for a fixed type member, etc. When that happens the `==` short-circuits to
`false` and a perfectly valid subtype relation is lost.

The principled invariant-equality check is mutual subtyping: two refs denote
the same type iff each is a subtype of the other. We keep the `==` fast-path
so the common case stays cheap and we avoid a recursive `isChild` call when
not needed.

  case (RefinementDecl.TypeMember(ln, lref), RefinementDecl.TypeMember(rn, rref)) =>
    ln == rn && (lref == rref || (ctx.isChild(lref, rref) && ctx.isChild(rref, lref)))

Tests
-----
* Added `Issue481A` / `Issue481AInt` / `Issue481AStr` fixtures to `TestModel`.
* Added a regression block in `SharedLightTypeTagTest` (so it runs across all
  Scala versions) that covers:
    - the direct repro from the issue body (`AInt <:< A { type T = Int }`),
    - the negative direction (`AInt` is not a subtype of `A { type T = String }`),
    - already-passing adjacent cases with upper-bounded abstract type members,
      to guard against regressions in surrounding paths.

Refs: zio#481
Co-authored-by: Cursor <cursoragent@cursor.com>
@CLAassistant

CLAassistant commented May 7, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

The previous run on 552e2f2 failed at "Prepare all required actions" with

  ##[error]The action carabiner-dev/actions@360ffa1eb909b...
  is not allowed in zio/izumi-reflect because all actions must be from
  a repository owned by zio, ...

This is a GitHub Actions allowlist check that fires on transitive
`uses:` resolution. The same workflow file is shared with other open
PRs (zio#586, zio#592) that passed CI on April 8 / May 1 -- nothing in this
PR's diff touches `.github/workflows` or any action reference. The
chain that resolves to `carabiner-dev/actions/ampel/verify` appears
to be transitively pulled in from one of the pinned actions and the
allowlist check seems to be intermittent.

Empty commit to bump the head SHA so the workflow re-resolves all
`uses:` references with a fresh action cache. If it fails again with
the same allowlist error, the fix has to come from a maintainer who
can either pin the offending transitive action to a specific SHA or
add `carabiner-dev/*` to the repository's allowed-actions list.

No code changes.

Signed-off-by: Milos M <MilosM348@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@MilosM348

Copy link
Copy Markdown
Author

Heads up to maintainers (cc @neko-kai @pshirshov): the failing CI on this PR is not caused by the diff. Every build (...) lane fails at the Prepare all required actions step with:

n##[error]The action carabiner-dev/actions/ampel/verify@360ffa1eb909b0105d4eccb6d6ef337911c34952 is not allowed in zio/izumi-reflect because all actions must be from a repository owned by zio, ... or match one of the patterns: 7mind/*, actions/*, ..., sbt/*, scalacenter/*, ...n

Where carabiner-dev comes from

It's pulled in transitively by sbt/setup-sbt@v1. Their action.yml at v1.1.21+ (added in late April / early May) now does:

L63:       id: ampel-verify
L69:       uses: carabiner-dev/actions/ampel/verify@360ffa1eb909b0105d4eccb6d6ef337911c34952 # v1.1.6
L71:         policy: "git+https://github.com/carabiner-dev/policies#signature/signature.json"```n
The org-level allowlist permits `sbt/*` (so `sbt/setup-sbt@v1` itself is fine) but **not** the transitive `carabiner-dev/*` it now invokes. Confirms via timeline:

* PR #586 ran 2026-04-08 ? all 12 build lanes green (sbt/setup-sbt@v1 still resolved to v1.1.18-ish)
* PR #592 ran 2026-05-01 ? still mostly green (1 unrelated test fail)
* PR #594 (this) ran 2026-05-07 ? 12/12 build lanes blocked at allowlist

### Fix options for maintainers

Either of:

1. Add `carabiner-dev/*` (or specifically `carabiner-dev/actions/ampel/verify@v*`) to the allowed-actions list at `Settings ? Actions ? General`.
2. Pin `sbt/setup-sbt` in `.github/workflows/build.yml` to a pre-carabiner SHA, e.g. `sbt/setup-sbt@v1.1.20`.

Forked PRs can't change `.github/workflows` (GitHub uses the base branch's workflow for fork PRs), so I can't apply option 2 from here. The PR diff itself touches only 3 source files: `LightTypeTagInheritance.scala` and two test files. None of the build / lint logic is exercised by allowlist failures, so the merit of the fix can still be reviewed from `Files changed`.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants