Skip to content

Conversation

@jiribenes
Copy link
Contributor

@jiribenes jiribenes commented Nov 21, 2025

See the PR comments.

@jiribenes jiribenes added the experiment Experimental branch, do not merge! label Nov 21, 2025
@jiribenes
Copy link
Contributor Author

jiribenes commented Nov 21, 2025

The following program breaks down for reasons I don't understand:

effect Raise(msg: String): Unit

def main() = try {
  def block(msg: String): Unit / {} = { do Raise(msg) }

  val f: () => Unit at raise = {
    val thn = box { block("first") }
    val els = box { block("second") }

    if (true) { // <  error: 
      thn       // <      Not allowed {raise}
    } else {    // <
      els       // <
    }
  }
} with raise: Raise { msg => panic(msg) }

thn and els are both inferred to be () => Unit at raise
Screenshot 2025-11-21 at 14 23 27

but the if-then-else stmt claims "Not allowed {raise}".

If I manually annotate the type of either thn or els with : () => Unit at raise, then everything works again. Strange.

@jiribenes
Copy link
Contributor Author

jiribenes commented Nov 21, 2025

The following commutation also works:

// [...]
    val thn = box { block("first") }
    val els = box { block("second") }

    def res() /* at raise */ = if (true) {
      thn()
    } else {
      els()
    }

    box res
  }
// [...]

@b-studios b-studios changed the title Experiment: disable boxed type capture subtyping Experiment: disable subtyping Nov 21, 2025
@b-studios
Copy link
Collaborator

I disabled subtyping in contravariant positions, since there we cannot (easily) insert coercions. In particular, we do not want to eta-expand functions to insert coercions.

@b-studios
Copy link
Collaborator

Current plan to insert coercions:

  1. since Typer annotated inferred all types but used subtyping in some positions, in the translation to core we now need to insert the coercions where necessary. In order to determine whether coercions are necessary, core-Transformer could pass down the expected return type of a statement / expression (hence only covariant coercions).
  2. If there is a type mismatch between the expected (Core Type?) and the type of the generated core expression that can be resolved by coercions, insert them there.

@jiribenes
Copy link
Contributor Author

jiribenes commented Nov 24, 2025

btw, in case we'd want other names than isZeroLike and isOneLike, we could try borrowing names from type theory with isUninhabited and isContractible :^)

@b-studios
Copy link
Collaborator

b-studios commented Nov 26, 2025

Quick status report: yesterday night, I tried to thread the expected type through the core.Transformer in order to insert coercions. I am not happy with the result since I am partially reimplementing type checking.

So I am currently tempted to go back to my original idea where Typer detects coercions and annotates them in the DB (yes... more side channeling---broken window).

Ideally, typer would perform elaboration. However, if we have unification based (HM-like) type inference, this is a bit tricky to achieve in general. How do other languages (like Koka) implement elaboration if things might become known only much later (and non-locally)?

It seems an elaborating typer is easier to implement in a bidirectional setting.

@b-studios
Copy link
Collaborator

Note: of course we can collect coercions also by Source and then display them in LSP as inlay information. This way some errors might be less confusing. For example in the test case

def foo(): Option[Int] = if (true) Some(42)

we infer a coercion from Some(42) to Unit since the else branch is missing.

@b-studios
Copy link
Collaborator

b-studios commented Nov 30, 2025

TODOs

  • Look into machine and LLVM at all.
  • potentially drop type annotations on Val and Let in core since typing should be unique, now that Match is annotated.
  • look into VMTests (I expect there are a lot of failing tests since we now add coercions regardless of the backend and I disabled some type-based optimizations)
  • implement proper type comparison on core (alpha equivalence, etc)
  • implement a proper type checker for core, now that the type system is simpler again.
  • consider replacing <>: tpe by <> match [tpe] {} and assign Nothing as a type for all holes.

@b-studios b-studios force-pushed the experiment/no-box-subtyping branch from 48d17b3 to 6005a10 Compare December 1, 2025 10:59
@b-studios
Copy link
Collaborator

Explicitly annotating match with the motif together with limited checks of type-correctness in core surfaced a few bugs:

A lot of times, transformations like Normalizer, RemoveTailResumptions, or DirectStyle change the result type and hence these transformations also need to change the motif annotated on the match. While this wasn't necessary before, this might have led to problems in PolymorphismBoxing, which we just haven't discovered yet.

@b-studios
Copy link
Collaborator

b-studios commented Dec 2, 2025

The VMTests look roughly ok with one exception: parsing_dollars that now suddenly has a lot of shifts and resumes that weren't there before.

   branches = 210,
-  pushedFrames = 68,
-  poppedFrames = 67,
+  pushedFrames = 1,
+  poppedFrames = 1,
   allocations = 0,
   variableWrites = 88,
-  resets = 2,
-  shifts = 68,
-  resumes = 66
+  resets = 1,
+  shifts = 1,
+  resumes = 0

Before we generated this code in core:

val v_r_7058: Nothing_383 = shift(p_7035) { {k_7106} =>
  return ()
};
v_r_7058 match {}

while now we generate

shift(p$14160: Prompt[Unit] @ {pCapt$14159}) { (){k$14237 @ resume$14236: Resume[Unit, Unit]} => 
  return ()
}

The former is arguably better, since it is trivially tail resumptive (empty match) in any continuation other than k_7106. Hence the other shift and resumes have been removed originally, but are now still there.

jiribenes added a commit that referenced this pull request Dec 2, 2025
def unify(c1: Capture, c2: Capture, ctx: ErrorContext): Unit = unify(CaptureSet(Set(c1)), CaptureSet(Set(c2)), ctx)
def unifyCapture(c1: Capture, c2: Capture, ctx: ErrorContext): Unit = unifyCaptures(CaptureSet(Set(c1)), CaptureSet(Set(c2)), ctx)

def isZeroLike(tpe: ValueType): Boolean = tpe match {
Copy link
Collaborator

Choose a reason for hiding this comment

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

we can delete those

case (s: ValueType, ValueTypeRef(t: UnificationVar), Invariant) => requireEqual(t, s, ctx)
// coercing is the last resort...
case (TBottom, to, Covariant, Some(coerce)) => coerce(Coercion.FromNothing(to))
case (from, TUnit, Covariant, Some(coerce)) => coerce(Coercion.ToUnit(from))
Copy link
Collaborator

Choose a reason for hiding this comment

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

delete this

@phischu phischu marked this pull request as ready for review December 10, 2025 14:07
Copy link
Collaborator

@phischu phischu left a comment

Choose a reason for hiding this comment

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

We fixed many things, this needs to be merged.

pushedFrames = 1,
poppedFrames = 1,
pushedFrames = 68,
poppedFrames = 67,
Copy link
Collaborator

Choose a reason for hiding this comment

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

We can merge but should investigate this.

enum Coercion {
case ToAny(from: ValueType)
case FromNothing(to: ValueType)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Again, let's merge but also investigate later whether we want to have coercions at all.

tpesAndTerms.foreach { case (tpe, term) =>
Context.requireSubtype(tpe, result, ErrorContext.Expected(tpe, result, term.getOrElse(Context.focus),
term.map(t => c => Context.coerce(t, c))))
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

If we do not insert coercions, we can also delete this duplicate check here, which improves errors.

* - each sequence contains a list of conditions that all have to match (conjunction).
*/
def compile(clauses: List[Clause]): core.Stmt = {
def compile(clauses: List[Clause], motif: ValueType): core.Stmt = {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I like "motif" but of course we can also use the english term "motive" that Conor used.

}

@targetName("preserveTypesStmt")
inline def preserveTypes(before: Stmt)(inline f: Stmt => Stmt): Stmt = {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we want to keep these in "production" as well? This might be costly.


// These are always never traversed (accept when checking on the module level, so we use the free structure)
case Empty
case Append(lhs: Constraints, rhs: Constraints)
Copy link
Collaborator

Choose a reason for hiding this comment

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

We are not yet done here (but can complete this in a follow up PR)! We need to actually check whether the constraints hold when encountering the declarations.

// // TODO factor out callLike things
// shouldEqual(data, retType)
// val Typing(argTypes, argCapt, argFree) = all(vargs, typecheck)
// valuesShouldEqual(paramTypes, argTypes)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm leaving this here, since we will need a very similar logic when checking constraints at the declaration site.

}
def setParent(u: UnionFind, child: Int, parent: Int) = {
with on[OutOfBounds].default { do raise(MissingValue(), "No node " ++ child.show) }
with default[OutOfBounds, Unit] { do raise(MissingValue(), "No node " ++ child.show) }
Copy link
Collaborator

Choose a reason for hiding this comment

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

It is a bit sad that we need the type annotations here.

@b-studios b-studios changed the title Experiment: disable subtyping Disable subtyping, introduce coercions, and type check core Dec 13, 2025
@b-studios
Copy link
Collaborator

b-studios commented Dec 13, 2025

@phischu once CI passes (which can take a time due to reparse tests), we can merge this

@b-studios
Copy link
Collaborator

b-studios commented Dec 13, 2025

I retract. The LLVM tests using files are still failing on my Mac M1. I would be nice to have an ARM runner; maybe we could set up one at some point.

b-studios pushed a commit that referenced this pull request Dec 13, 2025
There is a small caveat: I only enabled these for the PR CI, not for the
`master` branch CI. If interested in the latter, feel free to request
that too.

---

To be used in #1219.

- run all IO tests on macOS with LLVM
- run with `--debug` flag
- takes some 3m30s (like JS tests)

Here's the log:

<img width="645" height="274" alt="Screenshot 2025-12-13 at 17 50 31"
src="https://github.com/user-attachments/assets/df402a2c-bbb3-400f-94d4-da9ec511418d"
/>
Copy link
Collaborator

@b-studios b-studios left a comment

Choose a reason for hiding this comment

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

We should cherrypick / rebase the CI test added to master

b-studios and others added 2 commits December 14, 2025 09:03
There is a small caveat: I only enabled these for the PR CI, not for the
`master` branch CI. If interested in the latter, feel free to request
that too.

---

To be used in #1219.

- run all IO tests on macOS with LLVM
- run with `--debug` flag
- takes some 3m30s (like JS tests)

Here's the log:

<img width="645" height="274" alt="Screenshot 2025-12-13 at 17 50 31"
src="https://github.com/user-attachments/assets/df402a2c-bbb3-400f-94d4-da9ec511418d"
/>
@phischu phischu merged commit 39fb4df into master Dec 14, 2025
9 checks passed
@phischu phischu deleted the experiment/no-box-subtyping branch December 14, 2025 09:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

experiment Experimental branch, do not merge!

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants