Skip to content

Implement @auto("==") on types - #581

Merged
tjpalmer merged 19 commits into
mainfrom
auto-eq
Oct 12, 2026
Merged

tjpalmer merged 19 commits into
mainfrom
auto-eq

Conversation

@tjpalmer

@tjpalmer tjpalmer commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor
  • Support on types but not on individual methods in this pr
  • Support classes and sealed interface hierarchies
  • Validate against unsupported things
  • Don't generate an == if one already exists
  • Always use the name eq for generated methods in this pr
  • Add docs and a release note (even though we don't have one yet for @operator, I don't think)

Signed-off-by: Tom <tom@temper.systems>
Signed-off-by: Tom <tom@temper.systems>
Signed-off-by: Tom <tom@temper.systems>
Signed-off-by: Tom <tom@temper.systems>
Signed-off-by: Tom <tom@temper.systems>
Signed-off-by: Tom <tom@temper.systems>
Signed-off-by: Tom <tom@temper.systems>
Signed-off-by: Tom <tom@temper.systems>
Signed-off-by: Tom <tom@temper.systems>
Signed-off-by: Tom <tom@temper.systems>
Signed-off-by: Tom <tom@temper.systems>
Signed-off-by: Tom <tom@temper.systems>
Signed-off-by: Tom <tom@temper.systems>
Signed-off-by: Tom <tom@temper.systems>
Signed-off-by: Tom <tom@temper.systems>
Signed-off-by: Tom <tom@temper.systems>
Signed-off-by: Tom <tom@temper.systems>
Signed-off-by: Tom <tom@temper.systems>
private val vZero = Value(0, TInt)

internal fun dotHelperForOperator(member: Member): DotHelper {
fun dotHelperForOperator(member: Member): DotHelper {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted access.

import lang.temper.value.vVisibilitySymbol
import lang.temper.value.vWordSymbol
import lang.temper.value.valueContained
import lang.temper.value.void

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love having a semi-lock file of imports in every file. So fun.

fun addAutosIfWanted() {
reifiedType ?: return
// First check our own auto keys.
val autoKeys = typeDecl?.autoKeys() ?: listOf()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This abstracts the idea of there possibly being more than one, although this pr doesn't allow for that.

}
}
// Also check the parents' auto keys, just in case.
addAutoEqForSuperSealedIfWanted(autoKeys)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Realized probably best to skip this if we had one above, but it's rare that someone would label both, and we eventually dodge duplicate == anyway, so maybe ok.

eqBuiltinName.builtinKey in autoKeys && return
val typeShape = reifiedType!!.type2.definition as? TypeShape ?: return
// We care about this type only if it's concrete or sealed.
typeShape.abstractness == Abstractness.Concrete || typeShape.decl()?.isSealed() == true || return

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, this case is for when there ain't an @auto("=="), so it's ok to skip. We'll separately check each type we do add == to for seeing if it has only class or sealed subtypes, so if there's a mismatch there, it's handled elsewhere.

"Auto method not allowed with unsealed subtype(s): Sneaky!",
"Auto method not support for interface that's not sealed!",
"Auto method cannot support multiple supertype(s): Root1, Root2!",
"Type WhichToUse must implement eq from Root1. Maybe add `public eq(other: Root1): Boolean`!"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One reason I kept this test in generate code stage instead of just define was so I could see this later stage errors such as this one.

```log
Some are twins: true
All are twins: false
```

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I comment, this isn't testing backends so much as just testing that the code gen works anywhere.

We still should make passes to generate idiomatic operator methods in backends, though. Testing those would be a lot in backend tests.

@auto("==")
sealed interface High {
toString(): String;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Above is the only @auto.

Low2(to be) == Low2(to be): true
Low1(11) == Low3: false
Low3 == Low3: true
```

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But all the varieties seem to work fine.

override fun hashCode() = stateVector.hashCode()

companion object {
operator fun invoke(string: String) = Value(string, TString)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe should have added this long ago. I used it in the new frontend tree gen.

Signed-off-by: Tom <tom@temper.systems>
});
do {};
void;
Maybe extends AnyValue;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No pure virtual ==/eq for Maybe because it has an unsealed subtype. Maybe should just generate one anyway? I do give an error message for it, in any case.

@tjpalmer
tjpalmer marked this pull request as ready for review October 9, 2026 21:02
@tjpalmer tjpalmer mentioned this pull request Oct 10, 2026

<!-- snippet: builtin/@auto -->

<a name="builtin&#45;&#64;auto" class="snippet-anchor-name"></a>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love these docs

The *\@auto* decorator currently applies to a class or sealed interface,
indicating that the specified operator should be automatically implemented
for this type. In the future, it might be possible to specify on an
individual method for a specific operator.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had talked about a number of ways to get more fine-grained control.

Do you need to specify auto on the method or would you specify it on the class and then provide a stub method?

@auto("==")
export class C(...) {
  @operator("==")
  public equivalent(other: C): Boolean;  // pure virtual, so needs an implementation
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had talked about a number of ways to get more fine-grained control.

Do you need to specify auto on the method or would you specify it on the class and then provide a stub method?

I guess either could provide a name, yeah. I could see arguments either way.

than class or sealed interface descendents. Inheriting `@auto("==")` from
multiple sealed ancestor interface branches is also an error. Further, if a
manual `@operator("==")` method exists for a type, no automatic
implementation is injected.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we extend this to sealed interfaces with unsealed interface descendants that have a manual @operator("==") method?

}

@Suppress("SameParameterValue")
private fun errorTree(tree: Tree, leftIndex: Int, rightIndex: Int): CallTree =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Me too.

if (illegalSuperTypes.isEmpty()) {
if (illegalSuperTypes.isNotEmpty()) {
(typeShape as MutableTypeShape).superTypes.removeAll(illegalSuperTypes)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does make more sense.

I'm guessing this is part of an attempt to make sure later passes don't have to deal with super-type cycles.

That's not something people often write.

export class HasOwn extends Maybe {
@operator("==")
public eq(other: Maybe): Boolean {
// No equality here!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rude

@tjpalmer
tjpalmer merged commit bc75d83 into main Oct 12, 2026
2 checks passed
@tjpalmer
tjpalmer deleted the auto-eq branch October 12, 2026 20:39
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.

2 participants