Skip to content

Implement Typed IR#504

Draft
Derppening wants to merge 31 commits into
hkust-taco:hkmc2from
Derppening:enhance/typed-ir
Draft

Implement Typed IR#504
Derppening wants to merge 31 commits into
hkust-taco:hkmc2from
Derppening:enhance/typed-ir

Conversation

@Derppening
Copy link
Copy Markdown
Contributor

No description provided.

Copy link
Copy Markdown
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.

Some possible next immediate steps:

  • Update printer to show the erased types at variable and member declaration/definition sites.
  • Update Lowering so it generates erased types from parameter type annotations, to be used to annotate the corresponding VarSymbol.
  • Make sure we are never overriding an existing erased type in a given symbol by using softAssert, as a sanity check.

A subtlety we should get right: the erasure of annotated class parameter types should successfully propagate to their defining fields. Param has a ``fldSym` which can be used for this.

Comment thread hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala Outdated
new Rewriter(instId).applyBlock(ogBody),
mkReturnCall(restFunSym, restFunArgs))
val refreshedFvSymbols = dtorBranchFnFvs(branchId._1).map(s => s -> new VarSymbol(Tree.Ident(s"fv_${s.nme}")))
val refreshedFvSymbols = dtorBranchFnFvs(branchId._1).map(s => s -> new VarSymbol(Tree.Ident(s"fv_${s.nme}"), erasedType = N))
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.

It seems many cases like this one should carry over the previous erasedType somehow.

Comment thread hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala Outdated
@Derppening
Copy link
Copy Markdown
Contributor Author

Derppening commented Jun 1, 2026

The current task list (work items created by me, organized by AI):

  • Phase A — erasedType on Result (keystone infra) — ✅ committed

    • Enum redesign — PrimitiveType.Array + totalized sym; three-case ErasedType; ErasedType.sym; erasedType_!; Normalization → ObjectRef
    • Infra — Result extends HasErasedType; lazy val erasedType; abstract def on trait; Value's override val removed; this match over Tuple/Record/Instantiate/Value
    • Materialization sweep (Cat 1 + 2 + 3) — all applied, compiles clean, committed
    • Core sites (subTerm, ReflectionInstrumenter.assign, ValDefn.mk); join-point traps left N
  • Phase B — printer baseline (was C1) — captures post-A erasedType so later tightening shows as a diff

    • showErasedType toggle in Printer.scala (mirror showPurity, default OFF); render at variable + member declaration/definition sites
    • Commit one curated baseline test file (tuple/instantiate/lit/record/call/select/val-def, ≥1 case through Lifter) with post-A types as-is
  • Phase C — Annotation-driven erasedType + invariant guard (was Phase B; lands right after the B baseline)

    • Param-annotation → VarSymbol (Lowering erases param type annotation onto the VarSymbol)
    • Class-param erasure → defining field via Param.fldSym
    • softAssert no-clobber invariant (never override an existing symbol erasedType)
  • Phase D — WatBuilder consumes ErasedType (was C2; correctness harness)

    • Drive anyref cast targets from operand erasedType (additive + N-graceful); WASM goldens shift here
    • Optional explicit asserts when a known erasedType contradicts the required use-site type
  • Phase E — FuncRef + AnyFuncRef (was Phase D)

    • Add AnyFuncRef coarse constant, then FuncRef(params, result)
    • Fill Lambda → FuncRef
  • Phase F — refine residual inference (was Phase E)

    • Call return types (easy win: builtin-op result-type table, survey §6)
    • Select/DynSelect field/member types
    • rest params; Lifter capture symbols; function results
    • revisit resSym/l sites left N in Phase A

@LPTK
Copy link
Copy Markdown
Contributor

LPTK commented Jun 1, 2026

  • join-point traps left N

What does that mean?

  • C1 — showErasedType toggle in Printer.scala

This should be moved to phase B. In fact, it's the first thing yoiu should do, just so you can actually see what you're doing!

@Derppening
Copy link
Copy Markdown
Contributor Author

  • join-point traps left N

What does that mean?

erasedType = N, will be left for Phase D.

What does that mean?

  • C1 — showErasedType toggle in Printer.scala

This should be moved to phase B. In fact, it's the first thijng yoiu should do, just so you can actually see what you're doing!

Good point, I have updated to task list.

/** Refines the erased type if it was not previously refined, but allowing for idempotent refinements to the same
* type.
*
* A soft assert will be raised if the erased type was already refined to a different type.
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 comment is AI, right? Please clean up to remove dumb comments that just restate the code.

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.

Nope, that's me. Part of my "style" of writing comments is to document any invariants and side effects of a function with the assumption that nobody will bother to read the function implementation (think hovering over a function call to read the documentation).

I tried to make the docstring a bit more concise in dd94b46.

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.

Ok. By the way, why do you call this "refine"? It does not refine anything, it just inserts the relevant information where it used not to be.

Comment thread hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala

// * Used, eg, as the Assign receiver of intermediate computations whose result is not used
final class NoSymbol(using State) extends MaybeSymbol:
final class NoSymbol extends MaybeSymbol:
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.

It just occurred to me that this can just be an object.

Comment thread hkmc2/shared/src/test/mlscript/codegen/ErasedType.mls Outdated
//│ return id¹(tmp)
//│ ——————————————| Optimized IR |——————————————————————————————————————————————————————————————————————
//│ let id⁰; define id⁰ as fun id¹(a: ?) { return a }; return new Foo⁵(1)
//│ let id⁰: ?; define id⁰ as fun id¹(a: ?) { return a }; return new Foo⁵(1)
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.

There should probably also be a return type info on id¹.

doc"let ${names.mkString(", ")}; # ${print(body)}"
case s: TempSymbol => doc"${scope.allocateName(s)}${erasedTypeAnnot(s)}"
case s: LocalVarSymbol => doc"${symPrinter.printSymbol(s)}${erasedTypeAnnot(s)}"
case s: BlockMemberSymbol => doc"${symPrinter.printSymbol(s)}${s.tsym.fold(doc"")(erasedTypeAnnot(_))}"
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.

BMS members will have their type info displayed with the actual definition. There's no need to arbitrarily pick the tsym 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.

Ie, it's fine not to display any type for these here.

/** Refines the erased type, or raises a soft assertion if the type was already previously refined. */
def refineErasedType(newType: ErasedType)(using Line, FileName, Raise): Unit =
softAssert(erasedType.isEmpty, s"Cannot refine already-refined erased type $erasedType to $newType")
// TODO(Derppening): Restore `erasedType.isEmpty` once JS sanitization is converted into a pass
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.

Note: this comment is impossible to understand in isolation. Should say "once JS sanitization is converted into a pass, allowing us to only lower each program once".

case AnyRef(rsc: Bool, csym: ClassLikeSymbol | NoSymbol)

/** A reference to a function of a possibly-known shape. */
case FuncRef(sig: Opt[Ls[Opt[ErasedType]] -> Opt[ErasedType]])
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.

Cute use of ->, but having two separate fields will be cleaner (and more efficient).

* The parameters of curried functions are flattened into a single list: this is lossy
* for the arrow shape but does not affect the rendered return type, the only consumer
* today. An unannotated return remains `N` (refined in a later phase). */
private def refineFunDefnType(tsym: TermSymbol, paramLists: Ls[ParamList], sign: Opt[Term]): Unit =
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.

Can we stop calling this process "refine"?

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