Skip to content

Add WASM Backend Support with File Imports#472

Open
FetBoba wants to merge 16 commits intohkust-taco:hkmc2from
b-temirov:file-imports
Open

Add WASM Backend Support with File Imports#472
FetBoba wants to merge 16 commits intohkust-taco:hkmc2from
b-temirov:file-imports

Conversation

@FetBoba
Copy link
Copy Markdown
Contributor

@FetBoba FetBoba commented Apr 20, 2026

This PR enables compilation of .mls files 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.

@LPTK
Copy link
Copy Markdown
Contributor

LPTK commented Apr 20, 2026

Thanks. Please fix the merge conflicts and review your own diff. There should be no needless whitespace changes.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.Wasm configuration parsing and enforce where target can 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.

Comment thread hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala Outdated
Comment on lines +302 to +306
throw new IllegalStateException(s"Expected Wasm-targeted imported module at ${sourceFile.toString}")
dependencies += WasmDependency(
importPath,
emitWasm(importedModule, prelude, memo),
)
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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),
)

Copilot uses AI. Check for mistakes.
Comment thread hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala Outdated
Copy link
Copy Markdown
Contributor

@LPTK LPTK left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread hkmc2/jvm/src/test/scala/hkmc2/WasmModuleImportTest.scala Outdated
Comment thread hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala
case (sym, importPath) =>
resolveImportSource(importPath, moduleDir) match
case S(sourceFile) =>
val importedModule = elaborateImportedModule(sourceFile, prelude)
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.

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.

Comment thread hkmc2/shared/src/test/mlscript-compile/.gitignore Outdated
Comment thread hkmc2/shared/src/test/mlscript-compile/wasm/ImportValue.mls
Comment thread hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.wat
Comment thread hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.mls
Comment thread hkmc2/shared/src/test/mlscript/codegen/ImportMLsJS.mls Outdated
Copy link
Copy Markdown
Contributor

@Derppening Derppening left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @LPTK - The glue code looks okay but the changes to MLsCompiler.scala should be better explained and justified.

Comment thread hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/Ctx.scala Outdated
Comment thread hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala Outdated
Comment thread hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala
Comment thread hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala Outdated
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))"
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.

Didn't we say that this should be generated into a .wat file alongside the .mjs?

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.

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))"
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.

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
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 result is unexpected.

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.

4 participants