Add rsc / rsc? resource type modifiers - #560
Derppening wants to merge 61 commits into
Conversation
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>
I think that's a misunderstanding of what erased function types represent. They don't represent values... You should have caught this.
Nothing should silently "lose" their |
That's pretty serious. Why would you/Claude think this is acceptable? |
What is FCFT?! |
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 EDIT: On second thought, I don't know if using |
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>
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
That's poor wording - it means that
I recall us deciding that this (resource-ness on generic type parameters) wouldn't be part of the initial The PR description is also updated to reflect the current implementation.
I agree with your second thought. From what I remember when you first described resource-ness, |
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 |
Yes, resourceness of "generic" type parameters is not supported. Why would it not be supported on references to said parameters? What's the reason? |
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 |
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 |
| //│ ╔══[COMPILATION ERROR] Resource modifiers inside the parameters of a function type are not supported yet. | ||
| //│ ║ l.34: fun l: (rsc C) -> C | ||
| //│ ╙── ^ |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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] = |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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())`). |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Any reason rsc and mut have different precedences, leading to this weird logic?
There was a problem hiding this comment.
🔵 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
sharedSignatureDefinitionnow 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.
These "suppressed comments" do seem relevant. |
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>
| // * 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)) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Sounds like this "helpful" extension method just sits at the wrong abstraction level and should be removed.
There was a problem hiding this comment.
Removed in b368512. All its uses are converted so that they only match symbol types that can exist at that location.
| // `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. |
There was a problem hiding this comment.
This comment seems completely incongruous.
There was a problem hiding this comment.
Tried to address it in 045b3af - it now describes that a Cast node is inserted because Function <: Object.
Each call site is replaced with a match against symbol types that can reach it, and otherwise raises a soft assert.
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 TmarksTas a resource;rsc? Tleaves it to a runtime test.rscbinds tighter than|/&(rsc A | Bis(rsc A) | B) but looser than->'s LHS (rsc (A, B) -> Cis 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;mutandrscmay come in either order.rsccovers the argument lists (unlikemut, which binds tighter than application), sonew 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 arscfield ofProperNew/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 => ...)andrsc f(x)(a partial application) create a resource function value. A function value without a modifier isrsc?, sorsc?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); theAnnot.Resourceextractor yields the resource-ness they denote. The two keywords are matched asKeyword.RscLike. Seedoc/reference.md§ Resource Types.Erased types
Resource-ness is an
Opt[Bool]on the new sealed mixinHasRsc(AnyRef,ValueLike,Unknown):S(true)S(false)Nrsc?: not statically known, needs a runtime testUnknownbecomes acase class Unknown(rsc).PrimitiveandIncompatiblecarry no resource-ness.eraseSignthreads the modifier down to whatever the signature denotes, so it survives unions andforall. Members of a union take their own modifier if they have one.lubkeeps agreeing resource-ness and folds disagreement toN.needsCastnow checks two dimensions: the class hierarchy as before, then resource-ness iff both sides areHasRsc. Equal ⇒ no cast; widening intoNis free; narrowing out ofNis a checked cast;S(true)vsS(false)is an error, reported as an unrelated type (Cannot use a value of type 'rsc C' at an unrelated type 'C').type X = (rsc A) | B).resolveTpeSymAliasnow yieldsAliasMember(sym, ownRsc)per member. A member that cannot be resolved (recursion, type parameter, no definition) makes the alias erase toUnknownwith the join of the members' resource-ness. A member that is a function type, also under aforall, resolves toFunction, so an alias of one erases like the inline type.CanonicalErasedValueType.combineRsc):rscoverrides them (rsc Listwheretype List = Cons | Nil), whilersc?and no modifier keep them (rsc? Uwheretype U = rsc A | Bisrsc A | rsc? B, erasing torsc? lub(A, B)). A union written in place combines the same way with its members' own modifiers.x: rsc A) erases toUnknownunder that modifier; an unannotated one erases as before.Instantiategains a realrsc: Boolfield and is typed asValueLike(S(rsc), cls).Lambdagains arsc: Boolfield and is typed asFunction(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.LambdaRewritermarks the definition it lifts one into with@rsc(FunDefn.rsc), and FCFT instantiates the wrapper as a resource accordingly.Callgains arsc: Boolfield in its second parameter list, next tometadata, so patterns onCallare unchanged.Loweringsets it only on a call known to be under-applied, which is then typed asFunction(S(true)); an unmodified one staysFunction(N). A call that is exactly or over-applied is neverrsc(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(formerlyErasedFuncType) and itsSignature/CanonicalSignature(formerlyFuncRef/CanonicalFuncRef) have norscfield. A function value is an instance ofFunctionand has a resource-ness like any other instance:rscfor 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 paramlessdeclare) is rejected; one on a function type that types a value (a getter's result, aval, 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 nowErasedValueType; the memoizedcanonicalizeboth share lives in theCanonicalizablemixin. ATermSymbolstores either a value type or a signature, and a reference to a symbol with a signature isrsc? Function.HasErasedTypeandHasLateInitErasedTypeare removed: each symbol kind declares its ownerasedType, 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'sthisisrsc?.Validation
Checked in
Resolver(types) andElaborator(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,traverseSignnow 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 underrsc?, 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) whenrsc?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 toUnknown, so the modifier would be silently lost.Resource modifiers inside the parameters of a function type are not supported yet.A function type erases toFunctionwithout its parameter types, so(rsc C) -> Dwould silently lose the modifier. This applies wherever the function type is written, including a definition's separately written ordeclared 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.Normalizationindexes class params positionally under a hardassert, so dropping it would crash on a constructor pattern. In a contextual parameter list a bare tree is a type, sousing rsc Cis 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 asrscIdentity[Int32](1)is then rejected by the coercion into thersc Unknownslot.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: anewcarries 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 rscwith a refinement, resource instantiation in a staged module, and a resourceInstantiateorCallin the Wasm backend. The JS backend does not handleInstantiate.rscyet (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 emitsrscandrsc?to match the surface language spelling.fun test: Function = idnow takes a checked cast, and so do partial applications flowing into aFunctionslot, because function references arersc? Function(andrsc? FunctiontoFunctiontakes a downcast).thisinto a plain class (C) slot now inserts a checked cast.Instantiatedumps show the newrsc = falsefield.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 anew, and the remaining positions a modifier can be written in:as, where it has no effect because no cast is lowered, andispatterns andhandle, 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
class Box(x: rsc C)doesn't implyBoxmust bersc).rsc? Function, as for any symbol with a signature.rscflag 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.letbinding does not keep the erased type of its value, solet p = rsc f(x)binds an untypedp.InstantiateorLambdalike a non-resource equivalent, with no diagnostic; only theInstantiateTODO records this.rsc?asks for a checked cast, but neither backend tests resource-ness at runtime: the WasmCastarm tests only the class, and JS erases casts. This matchesCast's scaladoc, which already says a checked cast generates the same code as an unchecked one.