Repository navigation
Add Class Tag Optimization Pass - #555
Hidden character warning
NeilKleistGao wants to merge 55 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.
There was a problem hiding this comment.
[Astra] Rechecked at e785faeb3: this remains a P1 miscompilation. The new filters reject the original direct case, but mkWeb follows constructor arguments and destinations without maintaining those filters. A safe Box entry pulls the excluded A producer and mixed local/external consumer back into the web; taggedShapesOfMatchScrutinee then drops the unknown source again.
With :classTags mono, :js, and :noFreeze:
class Box(val x)
class A(val x)
class B(val x)
fun unknownThroughField(flag, external) =
let local = new A(0)
let holder = new Box(local)
let _ = @matchShapes(Box(_)) shape.match of holder, () => 0
let value = if flag then local else external
@matchShapes(A(_), B(_)) shape.match of value, () => 1, () => 2
unknownThroughField(false, new B(0)) returns 1 instead of 2, with no compilation diagnostic. Please enforce safety for the entire reachable web and validate every scrutinee source before lowering. Recorded in class-tags/ReviewFollowup.mls, unknownThroughField.
Regression evidence is committed locally in 2fbdb7344188d8303e81968994ae664a58c1380d (not pushed).
| // * 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.
|
Looks like the merge needs a golden test output update. |
Reproduce unsafe external-flow reentry, constructor field rewrites, and unlowered zero-branch shape matches. Preserve the shape-budget exhaustion reproducer behind :exit, verify anonymous recursive and curried-return branches, and refresh class-tag golden output after the upstream debug-printer changes.
There was a problem hiding this comment.
[Astra] Follow-up review of e785faeb399a76ec4a652d034d3c96b2afeb71b7: the review comments are not all satisfactorily addressed. I checked the code and reran the regression cases, including variants that exercise the new restrictions.
Of the eleven earlier Astra findings, seven are addressed within the newly restricted feature scope:
| Finding | Verification |
|---|---|
| Polymorphic allocation contexts | Original runtime regression now returns the expected difference of 1. |
| Dynamic field/null tag reads | The original case is now rejected because its constructor argument can come from outside. |
| Recursive branch expansion | Named branches are rejected; an anonymous branch making an ordinary recursive call compiles and returns 42. |
| Main blocks and class initializers | Shape matches in those scopes now have explicit diagnostics. |
| Nested functions | Named nested-function matches now have an explicit unsupported-scope diagnostic. |
| Partially covered function entries | The inspect/first/second regression now lowers all matches. |
| Curried branch calling semantics | Named curried branches are rejected; an anonymous branch returning another lambda preserves the returned function. |
Four Astra findings remain, plus the earlier budget comment:
- P1: Indirect flow still admits unknown scrutinees; the new case returns 1 instead of 2.
- P1: Constructor writes still produce stale tags; the new case returns 1 instead of 2.
- P2: Malformed intrinsic calls can escape lowering; a zero-branch call compiles and crashes at runtime.
- P2: Wasm remains ungated;
Todos.mlsstill records unresolved-field backend errors. - P2: The shape budget is enforced after expansion; a 21-field case exhausts a capped JVM before the budget diagnostic.
The other earlier review concerns—producer cycles, frozen-object handling, tuple elements, short-circuiting, fresh symbols for copied branches, and misplaced annotations—have corresponding guards or implementation changes. Frozen objects are explicitly unsupported while the option requires noFreeze; that is a support restriction, not implementation of tags on frozen objects.
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: