Add Class Tag Optimization Pass - #555
Hidden character warning
NeilKleistGao wants to merge 54 commits into
Conversation
|
I think one possible way to address the second problem is:
|
|
Hmm that does seem quite complicated. I don't think we actually need to transform the pattern matches in such a fine-grained way. We should be able to replace the entire deep pattern match by a single tag-check in one go. Indeed, the extracted ctor fields are only used for matching the shape itself, and not for something else; we should be able to not extract them at all. This might require a bit of a variable use analaysis, though. Or, better, we should simply find a way of annotating those "shape" pattern matches, which always discard the extracted fields, so the IR pass does not have to make a guess. Actually, now that I think of it, why don't we just generate code that already includes all the necessary info (in the form of annotations) for easily compiling the shape match away? Eg, something like: |
Actually, it seems that the @matchShapes(C(D(_)), C(E(1)), F(_))
if x is
C(D(_)) then ...
C(E(1)) then ...
F(_) then ...Those |
|
Suggestion from the meeting: Instead of Use an intrinsic like which can be easily desugared after shape analysis to |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Tag insertion currently fails with default-frozen JS objects and Wasm, while several shape-analysis paths can miscompile or fail to terminate.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 7
Open (9)
Recursive producer flows cause compiler stack overflow · New Tag assignment fails after immutable instance freezing · New Tuple shape validation ignores element shapes · New Shape checks lack short-circuit evaluation · New Copied match branches reuse bound symbols · New Uneliminated shape.match intrinsic can emit undefined calls · New Class-tag pass emits unsupported Wasm dynamic fields · New Shape alternatives cause unbounded Cartesian product growth · New No-effect warning suppressed for non-shape.match annotations · New
What changed in this PR
Adds an opt-in compiler pass that derives class-instance shapes, assigns runtime tags, and lowers shape.match calls into tag-based dispatch.
Changes:
- Adds
shape.match,@matchShapes, and class-tag configuration. - Extends flow analysis and compilation with tag insertion and match rewriting.
- Adds diff tests for basic, nested, functional, invalid, and subsumption cases.
| File | Description |
|---|---|
hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala |
Adds the :classTags test directive. |
hkmc2/shared/src/test/mlscript/invalml/InvalMLPrelude.mls |
Declares the new annotation and intrinsic. |
hkmc2/shared/src/test/mlscript/decls/Prelude.mls |
Adds public prelude declarations. |
hkmc2/shared/src/test/mlscript/class-tags/Subsumption.mls |
Tests shape subsumption. |
hkmc2/shared/src/test/mlscript/class-tags/Nested.mls |
Tests nested shapes and tuples. |
hkmc2/shared/src/test/mlscript/class-tags/Func.mls |
Tests interprocedural flows. |
hkmc2/shared/src/test/mlscript/class-tags/Basic.mls |
Tests basic tag generation and dispatch. |
hkmc2/shared/src/test/mlscript/class-tags/BadShapes.mls |
Tests malformed shape diagnostics. |
hkmc2/shared/src/test/mlscript/class-tags/Annotations.mls |
Tests annotation handling. |
hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala |
Represents shape annotations semantically. |
hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala |
Elaborates the annotation and intrinsic. |
hkmc2/shared/src/main/scala/hkmc2/Config.scala |
Adds pass configuration. |
hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala |
Recognizes the intrinsic during lowering. |
hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala |
Models shape-match data flow. |
hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala |
Supplies elaboration context to flow analysis. |
hkmc2/shared/src/main/scala/hkmc2/codegen/DeadParamElim.scala |
Supplies elaboration context to flow analysis. |
hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala |
Registers the class-tag pass. |
hkmc2/shared/src/main/scala/hkmc2/codegen/ClassTagsTransformer.scala |
Implements shape collection, tagging, and rewriting. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Add 14 reproduction cases for polymorphic tag loss, stale mutable shapes, unknown and null inputs, incomplete scope and web traversal, recursive and curried branches, surviving intrinsics, and Wasm tag fields. Preserve the expected results and current golden diagnostics with explicit fixme markers. Validation: ctest, focused class-tag and Wasm diff-tests, and hkmc2AllTests/test.
LPTK
left a comment
There was a problem hiding this comment.
[Astra] I reproduced 11 issues and committed 14 regression cases with golden outputs in ac25c9565. hkmc2AllTests/test passes with known defects explicitly marked :fixme; the compiler defects remain unresolved. The findings below include silent miscompilations in polymorphic allocation contexts, mutable field shapes, and mixed known/unknown scrutinees.
|
|
||
| private val producersInWeb = webs.iterator.flatMap(_.markedProducers).toSet | ||
|
|
||
| private val ctorsByResultId = producersInWeb.iterator.map(ctor => ctor.exprId -> ctor).toMap |
There was a problem hiding this comment.
[Astra] [P1] Preserve every polymorphic allocation context
With the default mono=false, multiple Ctor instances share an exprId. This map retains an arbitrary one, so wrap(A(0)) and wrap(B(0)) receive the same tag. The poly regression returns a difference of 0 instead of 1. Group and reconcile all contexts per allocation site; the entry collector at lines 46–48 has the same lossy mapping.
Reproduction: ReviewPolymorphism.mls.
There was a problem hiding this comment.
Fixed: the previous version only considered the mono case where one expirId maintains only one Ctor. Now the map maintains a list of Ctors.
| .flatMap(_.srcs) | ||
| .collect: | ||
| case ctor: Ctor => ctor | ||
| .toList.distinct.flatMap: ctor => | ||
| taggedShapesByProducer.getOrElse(ctor, Nil) |
There was a problem hiding this comment.
[Astra] [P1] Include unknown sources in the exhaustiveness check
Collecting only tagged constructors silently discards UnknownProd and untagged sources. A mixed local/external scrutinee consequently passes validation: unknownAlternative(0, new B(0)) returns unit instead of 2. Every possible source must have a compatible tag, otherwise compilation needs a diagnostic or semantic fallback.
Reproduction: unknownAlternative in ReviewShapes.mls.
There was a problem hiding this comment.
The previous web computation failed to filter out variables that may receive external data. Now it is fixed.
| // * Generate branch based on the branch function | ||
| private def mkBranch(branch: FunDefn, resultSymbol: TempSymbol): Block = | ||
| SymbolRefresher(Map.empty).apply(applyFunBodyLikeBlock(branch.body)).mapReturn: | ||
| case Return(result) => Assign(resultSymbol, result, End()) |
There was a problem hiding this comment.
[Astra] [P1] Prevent recursive branch expansion
mkBranch recursively transforms the branch body without tracking active expansions. A recursive branch that terminates after one runtime call causes a compiler StackOverflowError. Emitting ordinary branch calls would let the existing inliner handle recursion safely; otherwise expansion needs an explicit recursion guard. This is separate from cycle detection while computing producer shapes.
Reproduction: recursiveBranch in ReviewBranches.mls.
There was a problem hiding this comment.
Now we restrict shape.match to take only anonymous lambda functions as parameters, which is enough for the specialization.
| (entries.producers.nonEmpty || entries.consumers.nonEmpty) | ||
| && !entries.producers.exists(coveredProducers) | ||
| && !entries.consumers.exists(coveredConsumers) | ||
| then | ||
| val web = mkWeb(entries) |
There was a problem hiding this comment.
[Astra] [P2] Retain uncovered entries in partially covered functions
One covered entry suppresses all entries belonging to the function. Visiting inspect first covers the shared Box web, causing independent A and B matches in first and second to be omitted and rejected. Process the uncovered entries instead of skipping the whole function.
Reproduction: inspect, first, and second in ReviewScopes.mls.
There was a problem hiding this comment.
The previous web computation failed to connect some elements in the same function. Now it is fixed.



shape.matchand annotation@matchShapes.shape.matchinto tag checks.Problems to be addressed:How to track linearity (discussed in the meeting and decided to insert tags for class instances for pattern matching for now).Block IR uses a flattened pattern matching, which makes tag checks difficult. e.g.,It is flattened into:Assume thatCons(1, Cons(2, Cons(3, Cons(dyn, Cons(dyn, Cons(dyn, Nil))))))'s tag is0, we need to check if we want to replace the whole huge matching with the tag checking. It is not clear when to set the boundary to say that the inner irrelevant matchings are untouched. e.g.,how should we know whether we are now checking against a nested pattern (e.g.,C(D(...), E(...), F(...))), or this is just a user code?TODOs in future: