Conversation
| //│ (i32.const 42)) | ||
| //│ (ref.i31 | ||
| //│ (i32.const 1)))) | ||
| //│ (ref.cast (ref $Foo) |
There was a problem hiding this comment.
Previously we did
val objType = ctx.getFuncInfo_!(ctorFuncIdx).body.resultType_!
call(funcidx = ctorFuncIdx, as.map(argument), Seq(Result(objType.asValType_!)))
in Instantiate(...) and used the body result type for the call. But now we are using predeclared ConstructorInfo, which doesn't include body result type, so the call using RefType.anyref is less precise, hence the cast
private def directConstructorCall(
ctorInfo: ConstructorInfo,
args: Seq[Expr],
)(using Ctx): Expr =
ref.cast(
call(
funcidx = FuncIdx(ctorInfo.funcId),
operands = args,
returnTypes = Seq(Result(RefType.anyref)),
),
RefType(ctx.getType_!(ctorInfo.classBms), nullable = false),
)
There was a problem hiding this comment.
But now we are using predeclared
ConstructorInfo, which doesn't include body result type
Why not?
There was a problem hiding this comment.
We register ConstructorInfo in the prescan before body lowering so methods can reference constructors defined later in code. At that stage there is no emitted constructor body to get the result. But anyways removing cast seems to be fine.
hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala
Outdated
Show resolved
Hide resolved
|
Is this ready for review/merge? |
Not quite. Inheritance coming soon on top. Or should I raise inheritance separately? |
|
Yes, better keep the PRs small and merge them regularly. |
There was a problem hiding this comment.
Pull request overview
Adds WebAssembly backend support for instance/class methods on (non-inheriting) classes by lowering methods into standalone direct-call Wasm functions with an explicit receiver (this) parameter, plus a prescan that predeclares constructors/methods to support recursion and forward references.
Changes:
- Add prescan metadata registration for constructors and methods, and emit lowered method functions.
- Route constructor/method calls through direct
callto the lowered functions (instead of indirectcall_ref) where applicable. - Add/adjust Wasm golden tests for methods and for constructor calls in matching output.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| hkmc2/shared/src/test/mlscript/wasm/Methods.mls | New golden tests covering getters, nullary/callable methods, recursion, forward references, chaining, mutation, and error cases. |
| hkmc2/shared/src/test/mlscript/wasm/Matching.mls | Update golden WAT output to reflect direct constructor calls (call) rather than call_ref. |
| hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala | Implement method/ctor metadata prescan, emit lowered method funcs, and add direct-call lowering for constructors/methods and related selection/assignment handling. |
| hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/Ctx.scala | Add MethodInfo/ConstructorInfo metadata storage and accessors in the Wasm codegen context. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala
Outdated
Show resolved
Hide resolved
LPTK
left a comment
There was a problem hiding this comment.
The logic seems quite complicated, for what this achieves. But let's try to merge this soon anyway, as soon as you have fixed what I point out.
| private def registerDefnCallableMetadata(b: Block)(using Ctx, Raise, Scope): Unit = b match | ||
| case Define(defn: ClsLikeDefn, rst) => | ||
| registerClassCallableMetadata(defn) | ||
| registerDefnCallableMetadata(rst) | ||
| case Define(_, rst) => | ||
| registerDefnCallableMetadata(rst) | ||
| case Match(_, arms, dflt, rst) => | ||
| arms.foreach((_, body) => registerDefnCallableMetadata(body)) | ||
| dflt.foreach(registerDefnCallableMetadata) | ||
| registerDefnCallableMetadata(rst) | ||
| case Begin(_, rst) => | ||
| registerDefnCallableMetadata(rst) | ||
| case TryBlock(_, _, rst) => | ||
| registerDefnCallableMetadata(rst) | ||
| case Assign(_, _, rst) => | ||
| registerDefnCallableMetadata(rst) | ||
| case AssignField(_, _, _, rst) => | ||
| registerDefnCallableMetadata(rst) | ||
| case AssignDynField(_, _, _, _, rst) => | ||
| registerDefnCallableMetadata(rst) | ||
| case HandleBlock(_, _, _, _, _, _, _, rst) => | ||
| registerDefnCallableMetadata(rst) | ||
| case Label(_, _, body, rst) => | ||
| registerDefnCallableMetadata(body) | ||
| registerDefnCallableMetadata(rst) | ||
| case Scoped(_, body) => | ||
| registerDefnCallableMetadata(body) | ||
| case _: BlockTail => () |
There was a problem hiding this comment.
Wouldn't this work?
| private def registerDefnCallableMetadata(b: Block)(using Ctx, Raise, Scope): Unit = b match | |
| case Define(defn: ClsLikeDefn, rst) => | |
| registerClassCallableMetadata(defn) | |
| registerDefnCallableMetadata(rst) | |
| case Define(_, rst) => | |
| registerDefnCallableMetadata(rst) | |
| case Match(_, arms, dflt, rst) => | |
| arms.foreach((_, body) => registerDefnCallableMetadata(body)) | |
| dflt.foreach(registerDefnCallableMetadata) | |
| registerDefnCallableMetadata(rst) | |
| case Begin(_, rst) => | |
| registerDefnCallableMetadata(rst) | |
| case TryBlock(_, _, rst) => | |
| registerDefnCallableMetadata(rst) | |
| case Assign(_, _, rst) => | |
| registerDefnCallableMetadata(rst) | |
| case AssignField(_, _, _, rst) => | |
| registerDefnCallableMetadata(rst) | |
| case AssignDynField(_, _, _, _, rst) => | |
| registerDefnCallableMetadata(rst) | |
| case HandleBlock(_, _, _, _, _, _, _, rst) => | |
| registerDefnCallableMetadata(rst) | |
| case Label(_, _, body, rst) => | |
| registerDefnCallableMetadata(body) | |
| registerDefnCallableMetadata(rst) | |
| case Scoped(_, body) => | |
| registerDefnCallableMetadata(body) | |
| case _: BlockTail => () | |
| private def registerDefnCallableMetadata(b: Block)(using Ctx, Raise, Scope): Unit = b match | |
| case Define(defn: ClsLikeDefn, rst) => | |
| registerClassCallableMetadata(defn) | |
| registerDefnCallableMetadata(rst) | |
| case _ => b.subBlocks.foreach(subBlocks) |
There was a problem hiding this comment.
Please also update createDefnTypes similarly.
| case _: BlockMemberSymbol => disamb.getOrElse(l) | ||
| case sym => sym | ||
| resolved match | ||
| case _: ClassSymbol | _: ModuleOrObjectSymbol => true |
There was a problem hiding this comment.
Wouldn't this trigger in
:lot
object Test
Test
//│ —————————————| Lowered IR Tree |————————————————————————————————————————————————————————————————————
//│ Program:
//│ main = Scoped(syms = {member:Test⁰}):
//│ body = Define:
//│ defn = ClsLikeDefn:
//│ isym = object:Test¹
//│ sym = member:Test⁰
//│ k = Obj
//│ rest = Return: \
//│ res = Ref{disamb=object:Test¹}:
//│ l = member:Test⁰
//│ disamb = S of object:Test¹
//│ implct = true
//│ —————————————————| Output |—————————————————————————————————————————————————————————————————————————?
Again, self-references never have a disamb and always contain the inner symbol directly.
| sel.symbol.flatMap(_.asTrm).flatMap(ctx.getMethodInfo) | ||
|
|
||
| /** Returns whether `path` is an explicit owner reference such as a class or module path. */ | ||
| private def isDirectOwnerRef(path: Path): Bool = path match |
There was a problem hiding this comment.
Is this supposed to be named
| private def isDirectOwnerRef(path: Path): Bool = path match | |
| private def isSelfReference(path: Path): Bool = path match |
?
| val ctorCall = constructorInfoForPath(fun).map: ctorInfo => | ||
| directConstructorCall(ctorInfo, args.map(argument)) | ||
|
|
||
| val methodCall = fun match |
There was a problem hiding this comment.
This will always be computed, even when ctorCall is defined! You should move this expensive code to the orElse argument or make this val a def.
| ) | ||
| lhs match | ||
| case ownerRef: Value.Ref if isDirectOwnerRef(ownerRef) => | ||
| val resolvedOwner = ownerRef.l match |
There was a problem hiding this comment.
For the same reason stated above, I don't think this makes a lot of sense.
| //│ = 41 | ||
|
|
||
|
|
||
| :wat |
There was a problem hiding this comment.
We already have the :wat above, which I think is sufficient.
| :wat |
| case S(fieldSym) => | ||
| if ctx.getMethodInfo(fieldSym).nonEmpty then | ||
| errExpr( | ||
| Ls(msg"WatBuilder::returningTerm for AssignField(...) to class methods is not implemented yet" -> nme.toLoc), |
There was a problem hiding this comment.
I don't think we ever want to allow assigning to an object method. This error should be a standard compilation error, not a "not implemented yet".
Derppening
left a comment
There was a problem hiding this comment.
Just a few preliminary comments for now. I will need to look into this in more detail.
| enum MethodShape: | ||
| case Getter | ||
| case Callable |
There was a problem hiding this comment.
I didn't know there is a meaningful difference between bracketless vs bracketed invocations. AFAICT it seems like bracketless invocations are statically resolved and bracketed invocations are dynamically resolved.
@LPTK is this intended behavior?
There was a problem hiding this comment.
The resolution is the same. It's just the difference between taking an argument list, as in x.f(), and not taking one, as in x.f (the latter is declared as fun f = ...).
There was a problem hiding this comment.
TODO: we don't really need this enum; just store the number of parameter lists that each method accepts.
| case class MethodInfo( | ||
| ownerIsym: InnerSymbol, | ||
| dSym: TermSymbol, | ||
| funcId: SymIdx, | ||
| shape: MethodShape, | ||
| ) | ||
|
|
||
| case class ConstructorInfo( | ||
| classBms: BlockMemberSymbol, | ||
| funcId: SymIdx, | ||
| exportName: Opt[Str], | ||
| ) |
There was a problem hiding this comment.
Nit: Please add missing documentation.
This PR adds class-method support for non-inheriting classes by lowering instance methods to standalone direct-call wasm functions with an explicit
thisparameter, and a prescan that predeclares constructors and methods so self recursion, mutual recursion, later-method calls, and constructor calls to same or later classes work by construction.