Skip to content

Top-level constraints syntax#409

Merged
LPTK merged 9 commits intohkust-taco:hkmc2from
maximemulder:top-level-constraints
Mar 12, 2026
Merged

Top-level constraints syntax#409
LPTK merged 9 commits intohkust-taco:hkmc2from
maximemulder:top-level-constraints

Conversation

@maximemulder
Copy link

@maximemulder maximemulder commented Mar 10, 2026

Allow writing subtyping constraints at the top level, which is not meant to appear in real programs but can be useful to write tests (I do that !).

Example:

Int <: [Bot <: Top] => Int
//│ OK

A & B <: A
//│ OK

:te
A <: A & B
//│ ═══[TYPE ERROR] Cannot solve judgment A ≤ A ∧ B.

@maximemulder maximemulder force-pushed the top-level-constraints branch from b5b7230 to 410dcbc Compare March 10, 2026 09:19
@maximemulder maximemulder marked this pull request as ready for review March 10, 2026 09:20
Comment on lines +579 to +580
case InfixApp(lhs, Keywrd(op : (Keyword.`<:`.type | Keyword.`:>`.type)), rhs) =>
Term.SubConstr(constraint(lhs, op, rhs))
Copy link
Author

Choose a reason for hiding this comment

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

Conceptually clean pattern but I must say the Scala syntax is so heavy here 😔

Copy link
Contributor

Choose a reason for hiding this comment

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

How would you suggest to make it better?

Copy link
Author

Choose a reason for hiding this comment

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

I think it's mostly me being a x.type hater. I think values should be automatically lifted to singleton types when appearing in type positions. I also don´t like how Scala has both the bind @ pattern and bind : type syntaxes, the former should ideally subsume the later IMO. I also don't like how I had to use parentheses around the union.

Copy link
Contributor

@LPTK LPTK Mar 11, 2026

Choose a reason for hiding this comment

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

Yeah, all these things should be addressed in MLscript/Witt 😛

  InfixApp(lhs, Keywrd(Keyword.`<:` | Keyword.`:>` as op)), rhs) then
    Term.SubConstr of constraint(lhs, op, rhs)

(At least that's the syntax I envision.)

In Scala, x can't be used as a type due to the separate namespaces. When they introduced literal types, they briefly debated whether to make it 2.type (for consistency) or just 2, but they opted for the latter because it's just much nicer.

@maximemulder maximemulder changed the title top-level constraints Top-level constraints syntax Mar 10, 2026
@LPTK LPTK force-pushed the hkmc2 branch 2 times, most recently from 46c5ce1 to 414cf0c Compare March 10, 2026 15:42
Copy link
Contributor

@LPTK LPTK left a comment

Choose a reason for hiding this comment

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

Proposal: add to Predef

abstract class (<:<) Sub[A, B] with
  fun apply(x: A): B
abstract class (=:=) Eq[A, B] extends Sub[A, B] with
  fun apply(x: A): B
object Refl[A] extends Eq[A, A] with
  fun apply(x: A): A = x

@maximemulder
Copy link
Author

@LPTK Should I also add a Sup[A, B] base class ? I kind of want two because I like having the two syntaxes (subtype / supertype), but not sure how to do it (the following example obviously fails) 🤔

abstract class (<:<) Sub[A, B] with
  fun apply(x: A): B
abstract class (>:>) Sup[A, B] with
  fun apply(x: A): B
abstract class (=:=) Eq[A, B] extends Sub[A, B], Sup[A, B] with
  fun apply(x: A): B
object Refl[A] extends Eq[A, A] with
  fun apply(x: A): A = x

@LPTK
Copy link
Contributor

LPTK commented Mar 11, 2026

Should I also add a Sup[A, B] base class ?

Can you make it

type (>:>) Sup[A, B] = Sub[B, A]

?

IDK if this will work, but it should (if it doesn't, you could fix it).

@maximemulder
Copy link
Author

@LPTK Using the following works but the precedence is higher than most other type constructs so I have to put parentheses around most types on each sides (unions, intersections, arrows...), do I just accept the drawback or do you have an idea to solve that?

abstract class (<:<) Sub[A, B] with
  fun apply(x: A): B
abstract class (=:=) Eq[A, B] extends Sub[A, B] with
  fun apply(x: A): B
object Refl[A] extends Eq[A, A] with
  fun apply(x: A): A = x

type (>:>) Sup[A, B] = Sub[B, A]

@LPTK
Copy link
Contributor

LPTK commented Mar 12, 2026

Ah right, because the precedence is inferred from the first and last characters (< and >, here). I can see that it's not ideal, but maybe we can live with it for now?

Copy link
Contributor

@LPTK LPTK left a comment

Choose a reason for hiding this comment

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

Otherwise LGTM.

@maximemulder
Copy link
Author

Okay, I pushed a commit with the following changes:

  • Add classes and type alias to model subtyping constraints in the prelude.
  • Remove the <: and :> keywords in favor of the aforementioned types.
  • Change the constraint syntax from <: / :> to <:< / >:>.
  • Remove the Term.SubConstr term (constraints are now usual applications).

This simplifies the code at the cost of wrong/annoying precedence (requires parentheses for many composed types).

@LPTK
Copy link
Contributor

LPTK commented Mar 12, 2026

Actually if precedence is that important, why don't you just use S extends T? It parses correctly.

@maximemulder
Copy link
Author

@LPTK I think that would make the syntax of constrained and universal types ambiguous:

[A extends Int] => Int // Constrained or universal type (while the constraint is the same, should A be a new variable or not ?) ?

@LPTK
Copy link
Contributor

LPTK commented Mar 12, 2026

Yes, but I was talking about your top-level constraint testing use cases.

The <:< class and its use in constrained types can remain the same.

@maximemulder
Copy link
Author

maximemulder commented Mar 12, 2026

Hhhhm, I just checked and actually it doesn't seem to work for the top-level case either:

(~Nat & Int) extends Int
//│ FAILURE: Unexpected compilation error
//│ FAILURE LOCATION: subterm (Elaborator.scala:581)
//│ ╔══[COMPILATION ERROR] Unexpected infix use of keyword 'extends' here
//│ ║  l.62: 	(~Nat & Int) extends Int
//│ ╙──      	^^^^^^^^^^^^^^^^^^^^^^^^
//│ FAILURE: Unexpected parse error

I can make it work but I think I will just accept to use parentheses IMO.

@LPTK
Copy link
Contributor

LPTK commented Mar 12, 2026

Hhhhm, I just checked and actually it doesn't seem to work for the top-level case either

Well of course, since extends is a keyword, for that use case you'd need to adapt the Elaborator to handle it...

@maximemulder
Copy link
Author

maximemulder commented Mar 12, 2026

Edited my message at the same time as you sent yours 😅

Anyway, I kind of like having the same syntax for constraints in constrained types and at the top-level, parentheses are not that big of a deal IMO.

@LPTK LPTK merged commit a5b5a4f into hkust-taco:hkmc2 Mar 12, 2026
1 check passed
@LPTK LPTK deleted the top-level-constraints branch March 12, 2026 10:07
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