Add WASM Backend Support with File Imports#472
Add WASM Backend Support with File Imports#472FetBoba wants to merge 16 commits intohkust-taco:hkmc2from
Conversation
|
Thanks. Please fix the merge conflicts and review your own diff. There should be no needless whitespace changes. |
There was a problem hiding this comment.
Pull request overview
This PR adds a WASM compilation path for .mls modules, including emitting WAT and generated JS glue, and enables WASM-to-WASM file imports (while explicitly rejecting JS↔WASM imports for now).
Changes:
- Add
CompilationTarget.Wasmconfiguration parsing and enforce wheretargetcan be set. - Implement WASM compilation output (
.wat+ glue.mjs) with dependency compilation and wiring of imported module exports. - Extend WASM session export/import plumbing to use a per-module name (instead of a single REPL module name) and add tests/fixtures for WASM artifact emission and imports.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| hkmc2DiffTests/src/test/scala/hkmc2/WasmDiffMaker.scala | Updates WAT builder invocation to pass a module name for session exports/imports. |
| hkmc2/shared/src/test/mlscript/wasm/ClassMethods.mls | Updates expected diagnostic text for missing struct fields. |
| hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.mls | Adds a wasm-targeted compile fixture. |
| hkmc2/shared/src/test/mlscript-compile/wasm/ImportValue.mls | Adds a wasm-targeted import fixture (importer). |
| hkmc2/shared/src/test/mlscript-compile/wasm/IVal.mls | Adds a wasm-targeted import fixture (dependency). |
| hkmc2/shared/src/test/mlscript-compile/.gitignore | Ignores generated .wat artifacts under compile fixtures. |
| hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala | Disallows target overrides in @config(...) annotations (must be top-level #config). |
| hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala | Makes session exports use the current module name; improves field-select error messaging; updates program signature. |
| hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/Ctx.scala | Extends SessionExportCtx to carry a module name and threads it through collectors. |
| hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala | Adds a WASM compilation pipeline, dependency compilation, and glue generation. |
| hkmc2/shared/src/main/scala/hkmc2/Config.scala | Adds parsing support for target: CompilationTarget.{JS,Wasm} in config overrides. |
| hkmc2/jvm/src/test/scala/hkmc2/WasmModuleImportTest.scala | Adds an end-to-end JVM test validating wasm import wiring and instantiation. |
| hkmc2/js/src/test/scala/hkmc2/CompilerTest.scala | Adds a Scala.js test validating wasm artifact emission and refactors error detection helper. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| throw new IllegalStateException(s"Expected Wasm-targeted imported module at ${sourceFile.toString}") | ||
| dependencies += WasmDependency( | ||
| importPath, | ||
| emitWasm(importedModule, prelude, memo), | ||
| ) |
There was a problem hiding this comment.
Throwing IllegalStateException after emitting a diagnostic will crash compilation (including the JS-facing compiler wrapper, which expects diagnostics rather than exceptions). Instead, after raise(...), abort this Wasm compilation in a controlled way (e.g., return early/skip emission for this module, or use a dedicated non-throwing short-circuit similar to WatBuilder.program’s boundary + break pattern).
| throw new IllegalStateException(s"Expected Wasm-targeted imported module at ${sourceFile.toString}") | |
| dependencies += WasmDependency( | |
| importPath, | |
| emitWasm(importedModule, prelude, memo), | |
| ) | |
| else | |
| dependencies += WasmDependency( | |
| importPath, | |
| emitWasm(importedModule, prelude, memo), | |
| ) |
LPTK
left a comment
There was a problem hiding this comment.
There are very extensive changes to MLsCompiler.scala, but it's not clear why you are doing them, and some of them are actually harmful (elaborating imported files yourself instead of using the existing caching mechanism). Please address this. If you want to make such extensive changes to the logic, you should justify them and document the intent in the code.
| case (sym, importPath) => | ||
| resolveImportSource(importPath, moduleDir) match | ||
| case S(sourceFile) => | ||
| val importedModule = elaborateImportedModule(sourceFile, prelude) |
There was a problem hiding this comment.
Why are you changing the way imports work? This won't cache the imports, which is wrong. Furthermore, there is no reason not to share this logic between targets.
Derppening
left a comment
There was a problem hiding this comment.
I agree with @LPTK - The glue code looks okay but the changes to MLsCompiler.scala should be better explained and justified.
| import binaryen from "binaryen" | ||
|
|
||
|
|
||
| const __mlx_wat = "(module\n (type $Object (sub (struct (field $$tag (mut i32)))))\n (type $SimpleTarget (sub $Object (struct (field $$tag (mut i32)))))\n (type $SimpleTarget_init (func (param $this (ref null any)) (result (ref null any))))\n (type $SimpleTarget_ctor (func (result (ref null any))))\n (type $Unit (sub $Object (struct (field $$tag (mut i32)))))\n (type $Unit_init (func (param $this (ref null any)) (result (ref null any))))\n (type $Unit_ctor (func (result (ref null any))))\n (type $entry1 (func (result (ref null any))))\n (type $start (func))\n (global $Unit$inst (export \"Unit$inst\") (mut (ref null $Unit)) (ref.null $Unit))\n (func $SimpleTarget_init (type $SimpleTarget_init) (param $this (ref null any)) (result (ref null any))\n (block (result (ref null any))\n (nop)\n (nop)\n (return\n (local.get $this))))\n (func $SimpleTarget_ctor (export \"SimpleTarget\") (type $SimpleTarget_ctor) (result (ref null any))\n (local $this (ref null any))\n (block (result (ref null any))\n (local.set $this\n (struct.new_default $SimpleTarget))\n (struct.set $SimpleTarget $$tag\n (ref.cast (ref $SimpleTarget)\n (local.get $this))\n (i32.const 1))\n (drop\n (call $SimpleTarget_init\n (local.get $this)))\n (return\n (local.get $this))))\n (func $Unit_init1 (type $Unit_init) (param $this (ref null any)) (result (ref null any))\n (block (result (ref null any))\n (nop)\n (nop)\n (return\n (local.get $this))))\n (func $Unit_ctor1 (type $Unit_ctor) (result (ref null any))\n (local $this (ref null any))\n (block (result (ref null any))\n (local.set $this\n (struct.new_default $Unit))\n (struct.set $Unit $$tag\n (ref.cast (ref $Unit)\n (local.get $this))\n (i32.const 2))\n (drop\n (call $Unit_init1\n (local.get $this)))\n (return\n (local.get $this))))\n (func $start1 (type $start)\n (block\n (global.set $Unit$inst\n (ref.cast (ref null $Unit)\n (call $Unit_ctor1)))))\n (func $entry (export \"entry\") (type $entry1) (result (ref null any))\n (block (result (ref null any))\n (block\n (nop)\n (nop))\n (global.get $Unit$inst)))\n (elem $SimpleTarget_init declare func $SimpleTarget_init)\n (elem $SimpleTarget_ctor declare func $SimpleTarget_ctor)\n (elem $Unit_init1 declare func $Unit_init1)\n (elem $Unit_ctor1 declare func $Unit_ctor1)\n (elem $start1 declare func $start1)\n (elem $entry declare func $entry)\n (start $start1))" |
There was a problem hiding this comment.
Didn't we say that this should be generated into a .wat file alongside the .mjs?
There was a problem hiding this comment.
I see the file is there... You should obviously load the wat content from the file, not duplicate it here!
|
|
||
|
|
||
| const __mlx_wat = "(module\n (type $Object (sub (struct (field $$tag (mut i32)))))\n (type $SimpleTarget (sub $Object (struct (field $$tag (mut i32)))))\n (type $SimpleTarget_init (func (param $this (ref null any)) (result (ref null any))))\n (type $SimpleTarget_ctor (func (result (ref null any))))\n (type $Unit (sub $Object (struct (field $$tag (mut i32)))))\n (type $Unit_init (func (param $this (ref null any)) (result (ref null any))))\n (type $Unit_ctor (func (result (ref null any))))\n (type $entry1 (func (result (ref null any))))\n (type $start (func))\n (global $Unit$inst (export \"Unit$inst\") (mut (ref null $Unit)) (ref.null $Unit))\n (func $SimpleTarget_init (type $SimpleTarget_init) (param $this (ref null any)) (result (ref null any))\n (block (result (ref null any))\n (nop)\n (nop)\n (return\n (local.get $this))))\n (func $SimpleTarget_ctor (export \"SimpleTarget\") (type $SimpleTarget_ctor) (result (ref null any))\n (local $this (ref null any))\n (block (result (ref null any))\n (local.set $this\n (struct.new_default $SimpleTarget))\n (struct.set $SimpleTarget $$tag\n (ref.cast (ref $SimpleTarget)\n (local.get $this))\n (i32.const 1))\n (drop\n (call $SimpleTarget_init\n (local.get $this)))\n (return\n (local.get $this))))\n (func $Unit_init1 (type $Unit_init) (param $this (ref null any)) (result (ref null any))\n (block (result (ref null any))\n (nop)\n (nop)\n (return\n (local.get $this))))\n (func $Unit_ctor1 (type $Unit_ctor) (result (ref null any))\n (local $this (ref null any))\n (block (result (ref null any))\n (local.set $this\n (struct.new_default $Unit))\n (struct.set $Unit $$tag\n (ref.cast (ref $Unit)\n (local.get $this))\n (i32.const 2))\n (drop\n (call $Unit_init1\n (local.get $this)))\n (return\n (local.get $this))))\n (func $start1 (type $start)\n (block\n (global.set $Unit$inst\n (ref.cast (ref null $Unit)\n (call $Unit_ctor1)))))\n (func $entry (export \"entry\") (type $entry1) (result (ref null any))\n (block (result (ref null any))\n (block\n (nop)\n (nop))\n (global.get $Unit$inst)))\n (elem $SimpleTarget_init declare func $SimpleTarget_init)\n (elem $SimpleTarget_ctor declare func $SimpleTarget_ctor)\n (elem $Unit_init1 declare func $Unit_init1)\n (elem $Unit_ctor1 declare func $Unit_ctor1)\n (elem $start1 declare func $start1)\n (elem $entry declare func $entry)\n (start $start1))" | ||
| const __mlx_intrinsics_wat = "(module\n (type $div_impl (func (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any))))\n (type $eq_impl (func (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any))))\n (type $ge_impl (func (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any))))\n (type $gt_impl (func (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any))))\n (type $le_impl (func (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any))))\n (type $lt_impl (func (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any))))\n (type $minus_impl (func (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any))))\n (type $mod_impl (func (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any))))\n (type $neg_impl (func (param $arg (ref null any)) (result (ref null any))))\n (type $neq_impl (func (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any))))\n (type $not_impl (func (param $arg (ref null any)) (result (ref null any))))\n (type $plus_impl (func (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any))))\n (type $pos_impl (func (param $arg (ref null any)) (result (ref null any))))\n (type $times_impl (func (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any))))\n (func $div_impl1 (export \"div_impl\") (type $div_impl) (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any))\n (if (result (ref null any))\n (i32.and\n (ref.test (ref null i31)\n (local.get $lhs))\n (ref.test (ref null i31)\n (local.get $rhs)))\n (then\n (ref.i31\n (i32.div_s\n (i31.get_s\n (ref.cast (ref null i31)\n (local.get $lhs)))\n (i31.get_s\n (ref.cast (ref null i31)\n (local.get $rhs))))))\n (else\n (unreachable))))\n (func $eq_impl1 (export \"eq_impl\") (type $eq_impl) (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any))\n (if (result (ref null any))\n (i32.and\n (ref.test (ref null i31)\n (local.get $lhs))\n (ref.test (ref null i31)\n (local.get $rhs)))\n (then\n (ref.i31\n (i32.eq\n (i31.get_s\n (ref.cast (ref null i31)\n (local.get $lhs)))\n (i31.get_s\n (ref.cast (ref null i31)\n (local.get $rhs))))))\n (else\n (unreachable))))\n (func $ge_impl1 (export \"ge_impl\") (type $ge_impl) (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any))\n (if (result (ref null any))\n (i32.and\n (ref.test (ref null i31)\n (local.get $lhs))\n (ref.test (ref null i31)\n (local.get $rhs)))\n (then\n (ref.i31\n (i32.ge_s\n (i31.get_s\n (ref.cast (ref null i31)\n (local.get $lhs)))\n (i31.get_s\n (ref.cast (ref null i31)\n (local.get $rhs))))))\n (else\n (unreachable))))\n (func $gt_impl1 (export \"gt_impl\") (type $gt_impl) (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any))\n (if (result (ref null any))\n (i32.and\n (ref.test (ref null i31)\n (local.get $lhs))\n (ref.test (ref null i31)\n (local.get $rhs)))\n (then\n (ref.i31\n (i32.gt_s\n (i31.get_s\n (ref.cast (ref null i31)\n (local.get $lhs)))\n (i31.get_s\n (ref.cast (ref null i31)\n (local.get $rhs))))))\n (else\n (unreachable))))\n (func $le_impl1 (export \"le_impl\") (type $le_impl) (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any))\n (if (result (ref null any))\n (i32.and\n (ref.test (ref null i31)\n (local.get $lhs))\n (ref.test (ref null i31)\n (local.get $rhs)))\n (then\n (ref.i31\n (i32.le_s\n (i31.get_s\n (ref.cast (ref null i31)\n (local.get $lhs)))\n (i31.get_s\n (ref.cast (ref null i31)\n (local.get $rhs))))))\n (else\n (unreachable))))\n (func $lt_impl1 (export \"lt_impl\") (type $lt_impl) (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any))\n (if (result (ref null any))\n (i32.and\n (ref.test (ref null i31)\n (local.get $lhs))\n (ref.test (ref null i31)\n (local.get $rhs)))\n (then\n (ref.i31\n (i32.lt_s\n (i31.get_s\n (ref.cast (ref null i31)\n (local.get $lhs)))\n (i31.get_s\n (ref.cast (ref null i31)\n (local.get $rhs))))))\n (else\n (unreachable))))\n (func $minus_impl1 (export \"minus_impl\") (type $minus_impl) (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any))\n (if (result (ref null any))\n (i32.and\n (ref.test (ref null i31)\n (local.get $lhs))\n (ref.test (ref null i31)\n (local.get $rhs)))\n (then\n (ref.i31\n (i32.sub\n (i31.get_s\n (ref.cast (ref null i31)\n (local.get $lhs)))\n (i31.get_s\n (ref.cast (ref null i31)\n (local.get $rhs))))))\n (else\n (unreachable))))\n (func $mod_impl1 (export \"mod_impl\") (type $mod_impl) (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any))\n (if (result (ref null any))\n (i32.and\n (ref.test (ref null i31)\n (local.get $lhs))\n (ref.test (ref null i31)\n (local.get $rhs)))\n (then\n (ref.i31\n (i32.rem_s\n (i31.get_s\n (ref.cast (ref null i31)\n (local.get $lhs)))\n (i31.get_s\n (ref.cast (ref null i31)\n (local.get $rhs))))))\n (else\n (unreachable))))\n (func $neg_impl1 (export \"neg_impl\") (type $neg_impl) (param $arg (ref null any)) (result (ref null any))\n (if (result (ref null any))\n (ref.test (ref null i31)\n (local.get $arg))\n (then\n (ref.i31\n (i32.sub\n (i32.const 0)\n (i31.get_s\n (ref.cast (ref null i31)\n (local.get $arg))))))\n (else\n (unreachable))))\n (func $neq_impl1 (export \"neq_impl\") (type $neq_impl) (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any))\n (if (result (ref null any))\n (i32.and\n (ref.test (ref null i31)\n (local.get $lhs))\n (ref.test (ref null i31)\n (local.get $rhs)))\n (then\n (ref.i31\n (i32.ne\n (i31.get_s\n (ref.cast (ref null i31)\n (local.get $lhs)))\n (i31.get_s\n (ref.cast (ref null i31)\n (local.get $rhs))))))\n (else\n (unreachable))))\n (func $not_impl1 (export \"not_impl\") (type $not_impl) (param $arg (ref null any)) (result (ref null any))\n (if (result (ref null any))\n (ref.test (ref null i31)\n (local.get $arg))\n (then\n (ref.i31\n (i32.eqz\n (i31.get_s\n (ref.cast (ref null i31)\n (local.get $arg))))))\n (else\n (unreachable))))\n (func $plus_impl1 (export \"plus_impl\") (type $plus_impl) (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any))\n (if (result (ref null any))\n (i32.and\n (ref.test (ref null i31)\n (local.get $lhs))\n (ref.test (ref null i31)\n (local.get $rhs)))\n (then\n (ref.i31\n (i32.add\n (i31.get_s\n (ref.cast (ref null i31)\n (local.get $lhs)))\n (i31.get_s\n (ref.cast (ref null i31)\n (local.get $rhs))))))\n (else\n (unreachable))))\n (func $pos_impl1 (export \"pos_impl\") (type $pos_impl) (param $arg (ref null any)) (result (ref null any))\n (if (result (ref null any))\n (ref.test (ref null i31)\n (local.get $arg))\n (then\n (ref.i31\n (i31.get_s\n (ref.cast (ref null i31)\n (local.get $arg)))))\n (else\n (unreachable))))\n (func $times_impl1 (export \"times_impl\") (type $times_impl) (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any))\n (if (result (ref null any))\n (i32.and\n (ref.test (ref null i31)\n (local.get $lhs))\n (ref.test (ref null i31)\n (local.get $rhs)))\n (then\n (ref.i31\n (i32.mul\n (i31.get_s\n (ref.cast (ref null i31)\n (local.get $lhs)))\n (i31.get_s\n (ref.cast (ref null i31)\n (local.get $rhs))))))\n (else\n (unreachable))))\n (elem $div_impl1 declare func $div_impl1)\n (elem $eq_impl1 declare func $eq_impl1)\n (elem $ge_impl1 declare func $ge_impl1)\n (elem $gt_impl1 declare func $gt_impl1)\n (elem $le_impl1 declare func $le_impl1)\n (elem $lt_impl1 declare func $lt_impl1)\n (elem $minus_impl1 declare func $minus_impl1)\n (elem $mod_impl1 declare func $mod_impl1)\n (elem $neg_impl1 declare func $neg_impl1)\n (elem $neq_impl1 declare func $neq_impl1)\n (elem $not_impl1 declare func $not_impl1)\n (elem $plus_impl1 declare func $plus_impl1)\n (elem $pos_impl1 declare func $pos_impl1)\n (elem $times_impl1 declare func $times_impl1))" |
There was a problem hiding this comment.
Clearly we don't want to recompile these for on every single WASM module. It should be compiled as part of some RuntimeWASM.mls module (similar to Runtime.mls but used for WASM).
| SimpleTarget + 1 | ||
| //│ = 43 | ||
| SimpleTarget.value + 1 | ||
| //│ = NaN |
This PR enables compilation of
.mlsfiles to WASM modules with support for WASM-WASM file imports. What was done:Config Parsing, WASM Compilation (generating wat and glue mjs files), WASM compiled mls files can import files of the same type. Import between JS and WASM compiled files is currently rejected and will be implemented after this PR.