Skip to content

Add rsc / rsc? resource type modifiers - #560

Open
Derppening wants to merge 61 commits into
hkust-taco:hkmc2from
Derppening:enhance/rsc-type-modifier
Open

Derppening wants to merge 61 commits into
hkust-taco:hkmc2from
Derppening:enhance/rsc-type-modifier

Conversation

@Derppening

@Derppening Derppening commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

This description is generated by Fable 5.1 and edited by me.

This PR adds the surface syntax for marking a type as a resource and carries that information through elaboration into the erased types of the Block IR.

Syntax

  • rsc T marks T as a resource; rsc? T leaves it to a runtime test.
  • rsc binds tighter than |/& (rsc A | B is (rsc A) | B) but looser than ->'s LHS (rsc (A, B) -> C is a resource function, not a function taking a resource). A single resource parameter is written (rsc A) -> B, but is rejected for now (see below).
  • new [mut] [rsc] C(...) creates a resource instance; mut and rsc may come in either order. rsc covers the argument lists (unlike mut, which binds tighter than application), so new rsc C(1)(2) works. new rsc? is an error, since the resource-ness must be known during instantiation. The modifier is split off during desugaring into a rsc field of ProperNew/DynamicNew. Written outside the instantiation (rsc (new C())), it is not the instance's modifier and has no effect (a warning), whether or not it would be accepted inside.
  • rsc (x => ...) and rsc f(x) (a partial application) create a resource function value. A function value without a modifier is rsc?, so rsc? on one has no effect (a warning). On a call that is not known to leave some of its callee's parameter lists unapplied, the modifier has no effect either (a warning).

The modifiers elaborate to Term.Annotated(Annot.Modifier(kw), t); the Annot.Resource extractor yields the resource-ness they denote. The two keywords are matched as Keyword.RscLike. See doc/reference.md § Resource Types.

Erased types

Resource-ness is an Opt[Bool] on the new sealed mixin HasRsc (AnyRef, ValueLike, Unknown):

value meaning
S(true) a resource
S(false) not a resource (the unannotated default)
N rsc?: not statically known, needs a runtime test

Unknown becomes a case class Unknown(rsc). Primitive and Incompatible carry no resource-ness.

  • eraseSign threads the modifier down to whatever the signature denotes, so it survives unions and forall. Members of a union take their own modifier if they have one.
  • lub keeps agreeing resource-ness and folds disagreement to N.
  • needsCast now checks two dimensions: the class hierarchy as before, then resource-ness iff both sides are HasRsc. Equal ⇒ no cast; widening into N is free; narrowing out of N is a checked cast; S(true) vs S(false) is an error, reported as an unrelated type (Cannot use a value of type 'rsc C' at an unrelated type 'C').
  • Type aliases may write modifiers on their members (type X = (rsc A) | B). resolveTpeSymAlias now yields AliasMember(sym, ownRsc) per member. A member that cannot be resolved (recursion, type parameter, no definition) makes the alias erase to Unknown with the join of the members' resource-ness. A member that is a function type, also under a forall, resolves to Function, so an alias of one erases like the inline type.
  • A modifier on a reference to an alias combines with the members' own (CanonicalErasedValueType.combineRsc): rsc overrides them (rsc List where type List = Cons | Nil), while rsc? and no modifier keep them (rsc? U where type U = rsc A | B is rsc A | rsc? B, erasing to rsc? lub(A, B)). A union written in place combines the same way with its members' own modifiers.
  • A type-parameter reference with a modifier (x: rsc A) erases to Unknown under that modifier; an unannotated one erases as before.
  • Instantiate gains a real rsc: Bool field and is typed as ValueLike(S(rsc), cls).
  • Lambda gains a rsc: Bool field and is typed as Function(S(true)) if set, Function(N) otherwise. A resource lambda is not lifted into a function definition during lowering, so the slots it flows into see its resource-ness. LambdaRewriter marks the definition it lifts one into with @rsc (FunDefn.rsc), and FCFT instantiates the wrapper as a resource accordingly.
  • Call gains a rsc: Bool field in its second parameter list, next to metadata, so patterns on Call are unchanged. Lowering sets it only on a call known to be under-applied, which is then typed as Function(S(true)); an unmodified one stays Function(N). A call that is exactly or over-applied is never rsc (asserted where its type is computed). Passes that rebuild a call keep its flag, except where arguments are appended to it (EtaExpansion), since the result is a different value.

Function signatures carry no resource-ness; function values do. A signature describes how a definition is called and never types a value, so ErasedFuncSignature (formerly ErasedFuncType) and its Signature/CanonicalSignature (formerly FuncRef/CanonicalFuncRef) have no rsc field. A function value is an instance of Function and has a resource-ness like any other instance: rsc for a resource lambda or partial application, rsc? otherwise, including a reference to a definition. A signature is parsed once into its chain of function types (Elaborator.Sig). A modifier on a function type that a definition consumes as its own parameter list (head, curried inner, or paramless declare) is rejected; one on a function type that types a value (a getter's result, a val, a parameter) is kept.

Signatures and value types are kept apart. ErasedFuncSignature (Signature, CanonicalSignature) is no longer a subtype of the erased value types, whose root is now ErasedValueType; the memoized canonicalize both share lives in the Canonicalizable mixin. A TermSymbol stores either a value type or a signature, and a reference to a symbol with a signature is rsc? Function. HasErasedType and HasLateInitErasedType are removed: each symbol kind declares its own erasedType, matches on erased types are exhaustive over concrete symbol types, and a late-initialized erasure is set once through a setter that soft-asserts it was unset.

Since any class can be instantiated with new rsc, a class's this is rsc?.

Validation

Checked in Resolver (types) and Elaborator (definitions, parameters, type-parameter declarations, new). A modifier in a position that is not supported yet is still checked like any other, so it may report more than one error. To reach function-type parameters, traverseSign now traverses a tuple type's fields as types rather than as terms; no other difftest output changes.

  • Primitive type 'Int32' cannot be a resource. (a warning under rsc?, which has no effect). Applies through aliases.

  • A type takes at most one resource modifier. for a modifier written directly within another's reach (e.g. rsc rsc? T). A modifier on an alias reference or on a union combines with the members' instead (see above).

  • Every member of this type is a resource, so 'rsc?' has no effect. (a warning) when rsc? keeps member modifiers that already make every member a resource.

  • Resource modifiers on this type are not supported yet. for a type erasure would drop the modifier from: one with no erased counterpart (a negation, a tuple or a literal type), reached directly or through a union, and (), which is never a resource. The check asks erasure itself whether the modifier survives, so it does not duplicate the list of shapes.

  • Resource modifiers inside an intersection type are not supported yet. An intersection erases to Unknown, so the modifier would be silently lost.

  • Resource modifiers inside the parameters of a function type are not supported yet. A function type erases to Function without its parameter types, so (rsc C) -> D would silently lose the modifier. This applies wherever the function type is written, including a definition's separately written or declared signature (fun f: (rsc C) -> D); once a signature's parameter types reach the definition (Take parameter types from declaration #559 for a separately written one), the rejection can be lifted there. A modifier on the function type itself (rsc (C -> D)) is supported.

  • Resource modifiers apply to types, not to <kind> definitions. The definition is kept and compiles without the modifier.

  • Resource modifiers apply to types, not to parameters. The parameter is kept: ucs.Normalization indexes class params positionally under a hard assert, so dropping it would crash on a constructor pattern. In a contextual parameter list a bare tree is a type, so using rsc C is a resource type; only named or spread parameters take the modifier as the parameter's.

  • Resource modifiers apply to function values, not to a function definition's parameter lists.

  • An instance cannot be 'rsc?': it either is a resource or is not.

  • 'rsc' modifiers on type parameters are not supported yet. (or 'rsc?') on a declaration (class C[rsc A], class C[out rsc A], fun f[rsc A], [rsc A] -> T). A rejected declaration still declares the parameter, so its uses resolve. A use (x: rsc A) is allowed; a non-resource argument such as rscIdentity[Int32](1) is then rejected by the coercion into the rsc Unknown slot.

  • This annotation has no effect. (a warning), as for any other annotation, on a modifier written on an expression that is neither a lambda nor a call: a new carries its own (see above), and anything else, e.g. rsc 1, takes the modifier as an annotation that does nothing.

All of the above diagnostics are demonstrated via at least one difftest.

Not supported yet, rejected with an error: new! rsc, new rsc with a refinement, resource instantiation in a staged module, and a resource Instantiate or Call in the Wasm backend. The JS backend does not handle Instantiate.rsc yet (TODO): resources will carry a reference count there too.

Existing Difftest Changes

  • 'Unknown' becomes 'rsc? Unknown' in existing error messages, since nothing is known about an unannotated slot. The printer emits rsc and rsc? to match the surface language spelling.
  • fun test: Function = id now takes a checked cast, and so do partial applications flowing into a Function slot, because function references are rsc? Function (and rsc? Function to Function takes a downcast).
  • Passing this into a plain class (C) slot now inserts a checked cast.
  • Instantiate dumps show the new rsc = false field.
  • (WIP commit, in or out of this PR at the reviewer's call) A separately written signature is elaborated and resolved once for the declaration and the definition using it, so its diagnostics are no longer reported twice.

Tests

  • parser/Rsc.mls: precedence and token fusion.
  • codegen/ResourceTypes.mls: erasure, joins, casts, new rsc, this, function types, parameter-list consumption, resource lambdas and partial applications, each shape of a modifier written outside a new, and the remaining positions a modifier can be written in: as, where it has no effect because no cast is lowered, and is patterns and handle, which reject it.
  • codegen/ResourceTypeAliases.mls: modifiers inside aliases, unresolvable members, function-type members, alias-side checks.
  • codegen/ResourceModifierChecks.mls: every rejection above, including the contextual-parameter-list cases, modifiers inside type arguments (which are accepted, also on an alias's right-hand side that is not a type), function-type parameters, type parameters with and without variance, and the types erasure would drop a modifier from.

No type alias that erases without a diagnostic is left without a use site, so every golden shows either an error or what the alias erases to.

Known gaps

  • Resource-holding classes do not taint their classes (class Box(x: rsc C) doesn't imply Box must be rsc).
  • The FCFT eta wrapper is instantiated as a resource only for a lifted resource lambda; for other function values it waits on their resource-ness being resolved (TODO).
  • A reference to a function definition lifted from a resource lambda is still rsc? Function, as for any symbol with a signature.
  • A partial application's rsc flag is set from the callee's definition, but its type is computed from the callee's erased signature; without a signature, the flag has no effect and no warning is reported.
  • A let binding does not keep the erased type of its value, so let p = rsc f(x) binds an untyped p.
  • A separately written signature's parameter types do not yet reach the definition's own parameters (Take parameter types from declaration #559).
  • The JS backend compiles a resource Instantiate or Lambda like a non-resource equivalent, with no diagnostic; only the Instantiate TODO records this.
  • Narrowing out of rsc? asks for a checked cast, but neither backend tests resource-ness at runtime: the Wasm Cast arm tests only the class, and JS erases casts. This matches Cast's scaladoc, which already says a checked cast generates the same code as an unchecked one.

Derppening and others added 12 commits September 15, 2026 23:15
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Drop the "`rsc` is true if ..." doc bullets on the erased types and on
  `Instantiate`, as `HasRsc` documents what `rsc` encodes.
- Describe signatures in terms of parameter lists and function types
  rather than "arrows", including the wording from hkust-taco#504, and rename
  `wrapsArrow` to `wrapsFunTy` and `consumedArrows` to
  `consumedParamLists` to match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Derppening
Derppening requested a review from LPTK September 16, 2026 14:00
@LPTK

LPTK commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Function types carry no resource-ness. Whether a closure is a resource depends on what it captures, which is not implemented as part of this PR.

I think that's a misunderstanding of what erased function types represent. They don't represent values... You should have caught this.

So FuncRef/CanonicalFuncRef lose their rsc field

Nothing should silently "lose" their rsc field. Any rsc that can't be given proper meaning should yield an error.

@LPTK

LPTK commented Sep 16, 2026

Copy link
Copy Markdown
Contributor
  • Type arguments are erased, so Box[rsc C] loses the modifier; a primitive type argument also escapes the primitive check.

That's pretty serious. Why would you/Claude think this is acceptable?

@LPTK

LPTK commented Sep 16, 2026

Copy link
Copy Markdown
Contributor
  • The FCFT eta wrapper is instantiated as a non-resource with a TODO; it should be a resource iff the wrapped function is, once that is resolved.

What is FCFT?!

@LPTK

LPTK commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Since any class can be instantiated with new rsc, a class's this is rsc?.

Oof... we didn't think of that.

I'm afraid this is completely unacceptable. We probably can't just add runtime checks on every single use of this, can we? Unless maybe we can optimize them adequately...

EDIT: On second thought, I don't know if using this as a plain value is so common as to make this truly unacceptable. Provided we don't mess with uses of this that are only for calling methods and accessing fields (eg this.f), this might be ok.

Derppening and others added 7 commits September 17, 2026 00:32
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…arameters

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
once

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Derppening

Copy link
Copy Markdown
Contributor Author

I think that's a misunderstanding of what erased function types represent. They don't represent values... You should have caught this.

Doesn't erased function types represent the type of top-level functions, and non-free functions (i.e. lambdas) are represented as a simple (possibly rsc-modified) Function? If so, I think my edit is just poorly placed;

Nothing should silently "lose" their rsc field. Any rsc that can't be given proper meaning should yield an error.

That's poor wording - it means that FuncRef and CanonicalFuncRef no longer has a rsc modifier on them.

That's pretty serious. Why would you/Claude think this is acceptable?

I recall us deciding that this (resource-ness on generic type parameters) wouldn't be part of the initial rsc implementation - I added "not implemented" errors to all usages that would otherwise silently drop the rsc modifier.

The PR description is also updated to reflect the current implementation.

What is FCFT?!

FirstClassFunctionTransformer.

Oof... we didn't think of that.

I'm afraid this is completely unacceptable. We probably can't just add runtime checks on every single use of this, can we? Unless maybe we can optimize them adequately...

EDIT: On second thought, I don't know if using this as a plain value is so common as to make this truly unacceptable. Provided we don't mess with uses of this that are only for calling methods and accessing fields (eg this.f), this might be ok.

I agree with your second thought. From what I remember when you first described resource-ness, rsc C changes how the function body is compiled (compared to non-rsc), so it makes sense for the implicit this of class functions to be rsc? C - we would just have a runtime check that chooses which logic (RC-based or GC-based) to apply to the current object.

@LPTK

LPTK commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Doesn't erased function types represent the type of top-level functions, and non-free functions (i.e. lambdas) are represented as a simple (possibly rsc-modified) Function? If so, I think my edit is just poorly placed;

What does this have to do with "top-level functions" and "non-free functions"? AFAIK, an erased function "type" is really a signature (it should be renamed accordingly to avoid further future confusion), and it never types a value (it's not an ErasedValueType). So, the conjunction of the two assertions "Function types carry no resource-ness." and "Whether a closure is a resource depends on what it captures" as though the latter had any connection to the former doesn't make sense.

@LPTK

LPTK commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

I recall us deciding that this (resource-ness on generic type parameters) wouldn't be part of the initial rsc implementation - I added "not implemented" errors to all usages that would otherwise silently drop the rsc modifier.

Yes, resourceness of "generic" type parameters is not supported. Why would it not be supported on references to said parameters? What's the reason?

@LPTK

LPTK commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

FirstClassFunctionTransformer.

  • The FCFT eta wrapper is instantiated as a non-resource with a TODO; it should be a resource iff the wrapped function is, once that is resolved.

You should see that this is a symptom of a bigger problem: lambdas, which are not very different from normal object instances, should clearly have a rsc field, at least in the IR. Otherwise, how do you think we will reference-count them? The flag can probably be inferred later, but for now we'll need to just write resource lambdas as rsc (x => ....).

@LPTK

LPTK commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

we would just have a runtime check that chooses which logic (RC-based or GC-based) to apply to the current object

I think you're suggesting to double-compile every single method body (once for the RC branch, once for the GC branch), which is obviosuly unreasonable. The checks will have to be where this is used as a value, which is why I wrote what I wrote above.

Comment thread hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala Outdated
Comment on lines +39 to +41
//│ ╔══[COMPILATION ERROR] Resource modifiers inside the parameters of a function type are not supported yet.
//│ ║ l.34: fun l: (rsc C) -> C
//│ ╙── ^

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.

The error is duplicated.

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.

Implemented in 1cc917b by only elaborating the separate signature once (and fixing a FIXME), but with the caveat that the declaration must appear before the definition (which is how difftests are written right now anyways) - otherwise a variable name that shadows a type will crash the compiler (e.g. class C(); fun f: C -> C; fun f(C) = ...).

I am not sure if this fix is acceptable: it seems like an edge case to me and is an improvement over the existing behavior (where the compiler would just crash regardless). At the same time, I also think this could probably be refined and fully addressed with Claude/Codex.

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.

How would this be acceptable?! Why would this crash the compiler? Compiler crashes are never "acceptable". This is nonsense.

This is not a hard problem. Elaborating a signature only once is not difficult. At the very least, just make the elaborator reject signatures defined after definitions. You also don't need to use this hacky hash map. But you should get the scopes right: the fact that the parameter of f is called C or something else should have absolutely no effect on the result.

Comment thread hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala Outdated
Comment thread hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala Outdated
Comment thread hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala Outdated
Comment thread hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala Outdated
Comment thread hkmc2/shared/src/main/scala/hkmc2/codegen/ErasedType.scala Outdated
Comment thread hkmc2/shared/src/main/scala/hkmc2/codegen/ErasedType.scala Outdated
Comment thread hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala Outdated
Comment thread hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala Outdated
Comment thread hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala Outdated
Comment thread hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala Outdated
Comment thread hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala Outdated
Comment thread hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala Outdated
Comment on lines +95 to +100
val hasSeparateSignature = sym.trees.exists:
case td: Tree.TermDef => td.rhs.isEmpty && td.annotatedResultType.isDefined && td.paramLists.isEmpty
case _ => false
if !hasSeparateSignature then N
else sym.trees.collectFirst:
case td: Tree.TermDef if td.rhs.isDefined && td.annotatedResultType.isEmpty && td.typeParams.isEmpty => td

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 logic is very funky. Why not just a single collectFirst call?

* signature elaborated and resolved once for each, reporting every diagnostic in it twice. The signature is not
* shared with a definition that has type parameters of its own, as the signature may then refer to them.
*/
def sharedSignatureDefinition(sym: BlockMemberSymbol): Opt[Tree.TermDef] =

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 should probably be a lazy val in BlockMemberSymbol.

// * A declaration and the definition that consumes its signature share its elaboration, whichever comes
// * first in the block.
val sharesSignature = Elaborator.sharedSignatureDefinition(sym).exists: defn =>
(defn is td) || td.rhs.isEmpty && td.paramLists.isEmpty && td.annotatedResultType.isDefined

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.

Aren't these things already checked in sharedSignatureDefinition?

val sharesSignature = Elaborator.sharedSignatureDefinition(sym).exists: defn =>
(defn is td) || td.rhs.isEmpty && td.paramLists.isEmpty && td.annotatedResultType.isDefined
val s =
if sharesSignature then sharedSignatures.getOrElseUpdate(id.name, elabSignature)

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 logic is just weird. It looks like it might still elaborate a signature more than once in cases like

fun f = 1
fun f: Int

case _ => N

/** Applies the resource modifier `kw`, written at `kwLoc` inside an instantiation (`new rsc C()`),
* to the elaborated instantiation `inst`. A modifier has no effect if written outside one (`rsc (new C())`).

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.

No effect? Or does it raise an error? (It should.)


/** Splits the resource modifier off the body of a `new`.
*
* `rsc` parses looser than application, and `mut` tighter, so a `mut` written before `rsc` is moved back onto the

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.

Any reason rsc and mut have different precedences, leading to this weird logic?

Copilot AI left a comment

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.

🔵 Needs a closer look

The broad parser, type-system, IR, optimization, and backend refactor requires human validation, with regression coverage still needing attention.

Pull request overview

Adds rsc/rsc? resource modifiers across parsing, elaboration, erased types, Block IR, compiler passes, diagnostics, documentation, and golden tests.

Changes:

  • Adds resource syntax, precedence, validation, and documentation.
  • Tracks resource state through erased value types, calls, lambdas, and instantiation.
  • Updates lowering passes, backends, and diff-test snapshots.
File summaries
File Description
hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala Updates Block IR construction.
hkmc2/shared/src/test/mlscript/wasm/TailRecOptCasts.mls Updates resource-aware diagnostics.
hkmc2/shared/src/test/mlscript/wasm/Casts.mls Updates cast diagnostics.
hkmc2/shared/src/test/mlscript/wasm/Basics.mls Updates erased-type diagnostics.
hkmc2/shared/src/test/mlscript/parser/Rsc.mls Tests resource syntax and precedence.
hkmc2/shared/src/test/mlscript/codegen/ResourceTypeAliases.mls Tests resource-aware aliases.
hkmc2/shared/src/test/mlscript/codegen/ErasedTypes.mls Updates erased-type expectations.
hkmc2/shared/src/test/mlscript/codegen/BuiltinSymbols.mls Updates instantiation snapshots.
hkmc2/shared/src/main/scala/hkmc2/syntax/Tree.scala Carries resource modifiers through syntax.
hkmc2/shared/src/main/scala/hkmc2/syntax/ParseRule.scala Registers resource modifiers.
hkmc2/shared/src/main/scala/hkmc2/syntax/Parser.scala Fuses the rsc? token.
hkmc2/shared/src/main/scala/hkmc2/syntax/Keyword.scala Defines resource keywords and precedence.
hkmc2/shared/src/main/scala/hkmc2/semantics/ups/SplitCompiler.scala Adapts temporary symbol creation.
hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/Normalization.scala Preserves new IR type information.
hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala Adds resource annotation handling.
hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala Separates value types from signatures.
hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala Validates resource type modifiers.
hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala Elaborates modifiers and signatures.
hkmc2/shared/src/main/scala/hkmc2/invalml/InvalML.scala Adapts temporary symbol construction.
hkmc2/shared/src/main/scala/hkmc2/codegen/WorkerWrapper.scala Sets call resource state.
hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala Handles or rejects resource IR.
hkmc2/shared/src/main/scala/hkmc2/codegen/UsedVarAnalyzer.scala Adapts instantiation matching.
hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala Preserves signatures and resource state.
hkmc2/shared/src/main/scala/hkmc2/codegen/SymbolRefresher.scala Preserves symbol erasures.
hkmc2/shared/src/main/scala/hkmc2/codegen/ReflectionInstrumenter.scala Rejects staged resource instantiation.
hkmc2/shared/src/main/scala/hkmc2/codegen/Printer.scala Prints resource-aware IR types.
hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala Lowers resource calls, lambdas, and instances.
hkmc2/shared/src/main/scala/hkmc2/codegen/Lifter.scala Preserves resource flags during lifting.
hkmc2/shared/src/main/scala/hkmc2/codegen/LambdaRewriter.scala Marks lifted resource lambdas.
hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala Adapts JS generation to new IR.
hkmc2/shared/src/main/scala/hkmc2/codegen/HandlerLowering.scala Updates generated calls and temporaries.
hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala Adapts flow analysis patterns.
hkmc2/shared/src/main/scala/hkmc2/codegen/FirstClassFunctionTransformer.scala Propagates resource function wrappers.
hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala Handles resource state during expansion.
hkmc2/shared/src/main/scala/hkmc2/codegen/ErasedType.scala Implements resource-aware erased types.
hkmc2/shared/src/main/scala/hkmc2/codegen/deforest/Rewrite.scala Preserves signatures in rewrites.
hkmc2/shared/src/main/scala/hkmc2/codegen/DeadParamElim.scala Rewrites erased signatures safely.
hkmc2/shared/src/main/scala/hkmc2/codegen/ClassParamFlattener.scala Preserves instantiation resource flags.
hkmc2/shared/src/main/scala/hkmc2/codegen/BufferableTransform.scala Updates generated IR calls.
hkmc2/shared/src/main/scala/hkmc2/codegen/BlockTraverser.scala Traverses expanded IR nodes.
hkmc2/shared/src/main/scala/hkmc2/codegen/BlockTransformer.scala Preserves resource fields in transformations.
hkmc2/shared/src/main/scala/hkmc2/codegen/BlockSimplifier.scala Preserves resource state while simplifying.
hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala Extends core Block IR and typing.
hkmc2/shared/src/main/scala/hkmc2/AsyncLowering.scala Uses separated function signatures.
doc/reference.md Documents resource syntax and behavior.
Review details

Suppressed comments (2)

hkmc2/shared/src/test/mlscript/codegen/ResourceModifierChecks.mls:211

  • The test comment still describes the old double-elaboration behavior, but sharedSignatureDefinition now shares the signature and the golden output reports one diagnostic. Update the comment to match the behavior this regression test now exercises.
    hkmc2/shared/src/test/mlscript/codegen/ResourceModifierChecks.mls:738
  • This comment still says the diagnostic is duplicated, while the shared-signature path and golden output now report it once. Update the explanation so the test documents its current behavior.
  • Files reviewed: 47/47 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread hkmc2/shared/src/test/mlscript/codegen/ErasedTypes.mls
@LPTK

LPTK commented Sep 18, 2026

Copy link
Copy Markdown
Contributor
  • The test comment still describes the old double-elaboration behavior, but sharedSignatureDefinition now shares the signature and the golden output reports one diagnostic. Update the comment to match the behavior this regression test now exercises.
    hkmc2/shared/src/test/mlscript/codegen/ResourceModifierChecks.mls:738
  • This comment still says the diagnostic is duplicated, while the shared-signature path and golden output now report it once. Update the explanation so the test documents its current behavior.

These "suppressed comments" do seem relevant.

Derppening and others added 7 commits September 20, 2026 18:51
This commit reverts 8f7ba24, and updates the comment to describe what
the test demonstrates.

Claude is only used to restore the test body - comment is
manually-written.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
While type aliases in type position are resolved and canonicalized, they
never represent a legal value when used in value position, so type them
as `rsc? Unknown`.
`ClassSymbol` used to define `erasedType` as itself with an unknown
resource-ness. This is incorrect in two ways:

- Resource-ness on a type is really only meaningful on an instance of
  the class, not on the class itself;
- It conflicts the reading of `erasedType` on other symbols, where it
  represents the type of the symbol value - except when used to
  represent `this` within a class, via `Value.This`.

Remove `ClassSymbol.erasedType` and instead have its use-sites derive
the correct erased type.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…asedType`

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
For consistency with `Call`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment on lines +584 to +586
// * A class symbol may appear as a value as a reference to `this` in a class, and so carries the erased type of its
// * own class, but we don't have enough information to determine its resource-ness.
case c: ClassSymbol => S(ErasedType.ValueLike(rsc = N, c))

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 is extremely fishy. What does "may appear as a value as a reference to this" even mean? It's no longer true in the IR.

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.

Sounds like this "helpful" extension method just sits at the wrong abstraction level and should be removed.

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.

Removed in b368512. All its uses are converted so that they only match symbol types that can exist at that location.

Comment thread hkmc2/shared/src/main/scala/hkmc2/codegen/Lifter.scala Outdated
Comment thread hkmc2/shared/src/test/mlscript/codegen/ErasedTypes.mls Outdated
Comment on lines +884 to +885
// `new Object` creates a non-resource instance of `Object`, so a cast is inserted to coerce it to `Function`.
// Note that this cast has nothing to do with resource-ness - both `Object` and `Function` are non-resource types.

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 seems completely incongruous.

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.

Tried to address it in 045b3af - it now describes that a Cast node is inserted because Function <: Object.

This branch has not been deployed

No deployments
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.

3 participants