From c8d81574e747f3ed249646616b11041b9733f991 Mon Sep 17 00:00:00 2001 From: FetBoba Date: Sat, 18 Apr 2026 08:00:44 +0800 Subject: [PATCH 01/12] Added parsing of target config flag --- hkmc2/shared/src/main/scala/hkmc2/Config.scala | 16 +++++++++++++++- .../main/scala/hkmc2/semantics/Elaborator.scala | 13 +++++++++++-- .../src/test/mlscript/backlog/ToTriage.mls | 2 +- .../shared/src/test/mlscript/codegen/Spreads.mls | 4 ++-- .../src/test/mlscript/lifter/ClassInFun.mls | 2 +- 5 files changed, 30 insertions(+), 7 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/Config.scala b/hkmc2/shared/src/main/scala/hkmc2/Config.scala index 4053e7af34..bedf9abdec 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/Config.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/Config.scala @@ -150,6 +150,18 @@ object ConfigParser: source = Diagnostic.Source.Compilation)) N + private def parseCompilationTarget(tree: Tree)(using Raise): Opt[CompilationTarget] = + tree.deparenthesized match + case Sel(Ident("CompilationTarget"), Ident("JS")) | Ident("JS") => + S(CompilationTarget.JS) + case Sel(Ident("CompilationTarget"), Ident("Wasm")) | Ident("Wasm") => + S(CompilationTarget.Wasm) + case _ => + raise(ErrorReport( + msg"Expected CompilationTarget.JS or CompilationTarget.Wasm" -> tree.toLoc :: Nil, + source = Diagnostic.Source.Compilation)) + N + /** Parse the `None`/`Some(...)` syntax for optional config fields. * Also accepts unwrapped values as a convenience (treated as `Some(value)`). */ private def parseOpt[A](tree: Tree)(parseInner: Tree => Opt[A])(using Raise): Opt[Opt[A]] = tree match @@ -237,6 +249,9 @@ object ConfigParser: case "commentGeneratedCode" => parseBool(value) match case S(v) => _.copy(commentGeneratedCode = v) case N => identity + case "target" => parseCompilationTarget(value) match + case S(v) => _.copy(target = v) + case N => identity case "effectHandlers" => cfg => parseOpt(value)(v => parseEffectHandlers(v, cfg.effectHandlers)) match @@ -264,4 +279,3 @@ object ConfigParser: source = Diagnostic.Source.Compilation)) identity end ConfigParser - diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala index 9cd37b6c7f..1d1846099d 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala @@ -378,8 +378,17 @@ extends Importer with ucs.SplitElaborator: def annot(tree: Tree): Ctxl[Opt[Annot]] = tree match case Keywrd(kw @ (Keyword.`abstract` | Keyword.`declare` | Keyword.`data` | Keyword.`staged`)) => S(Annot.Modifier(kw)) case App(Ident("config"), Tup(args)) => - val modify = ConfigParser.parseOverrides(args) - S(Annot.Config(modify)) + val hasTargetOverride = args.collectFirst: + case InfixApp(Ident("target"), Keywrd(Keyword.`:`), _) => () + .nonEmpty + if hasTargetOverride then + raise(ErrorReport( + msg"`target` can only be configured with a top-level #config(...) directive" -> tree.toLoc :: Nil, + source = Diagnostic.Source.Compilation)) + N + else + val modify = ConfigParser.parseOverrides(args) + S(Annot.Config(modify)) case _ => term(tree) match case Term.Error => N case trm => diff --git a/hkmc2/shared/src/test/mlscript/backlog/ToTriage.mls b/hkmc2/shared/src/test/mlscript/backlog/ToTriage.mls index 61dca63f7d..b98d0534e4 100644 --- a/hkmc2/shared/src/test/mlscript/backlog/ToTriage.mls +++ b/hkmc2/shared/src/test/mlscript/backlog/ToTriage.mls @@ -233,7 +233,7 @@ id(Foo)(1) // ——— ——— ——— data class FooSpd(...args) -//│ ╔══[INTERNAL ERROR] Compiler reached an unexpected state at 'Elaborator.scala:1306': mismatched param list lengths List() vs List(term:FooSpd/args) +//│ ╔══[INTERNAL ERROR] Compiler reached an unexpected state at 'Elaborator.scala:1315': mismatched param list lengths List() vs List(term:FooSpd/args) //│ ╟── The compilation result may be incorrect. //│ ╙── This is a compiler bug; please report it to the maintainers. diff --git a/hkmc2/shared/src/test/mlscript/codegen/Spreads.mls b/hkmc2/shared/src/test/mlscript/codegen/Spreads.mls index cc91f090f8..08d53037dd 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/Spreads.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/Spreads.mls @@ -58,7 +58,7 @@ foo(0, ...a) data class A(...r) let x = new A(1, 2, 3) x.r -//│ ╔══[INTERNAL ERROR] Compiler reached an unexpected state at 'Elaborator.scala:1306': mismatched param list lengths List() vs List(term:A/r) +//│ ╔══[INTERNAL ERROR] Compiler reached an unexpected state at 'Elaborator.scala:1315': mismatched param list lengths List() vs List(term:A/r) //│ ╟── The compilation result may be incorrect. //│ ╙── This is a compiler bug; please report it to the maintainers. //│ ═══[RUNTIME ERROR] Error: Access to required field 'r' yielded 'undefined' @@ -70,7 +70,7 @@ x.r data class A(...r) with fun getR = r new A(1, 2, 3).getR -//│ ╔══[INTERNAL ERROR] Compiler reached an unexpected state at 'Elaborator.scala:1306': mismatched param list lengths List() vs List(term:A/r) +//│ ╔══[INTERNAL ERROR] Compiler reached an unexpected state at 'Elaborator.scala:1315': mismatched param list lengths List() vs List(term:A/r) //│ ╟── The compilation result may be incorrect. //│ ╙── This is a compiler bug; please report it to the maintainers. //│ ═══[RUNTIME ERROR] ReferenceError: r is not defined diff --git a/hkmc2/shared/src/test/mlscript/lifter/ClassInFun.mls b/hkmc2/shared/src/test/mlscript/lifter/ClassInFun.mls index 75612aa703..ce1953616d 100644 --- a/hkmc2/shared/src/test/mlscript/lifter/ClassInFun.mls +++ b/hkmc2/shared/src/test/mlscript/lifter/ClassInFun.mls @@ -291,7 +291,7 @@ fun f(x) = let res = f(0) res.a res.b -//│ ╔══[INTERNAL ERROR] Compiler reached an unexpected state at 'Elaborator.scala:1306': mismatched param list lengths List() vs List(term:A/r) +//│ ╔══[INTERNAL ERROR] Compiler reached an unexpected state at 'Elaborator.scala:1315': mismatched param list lengths List() vs List(term:A/r) //│ ╟── The compilation result may be incorrect. //│ ╙── This is a compiler bug; please report it to the maintainers. //│ ═══[RUNTIME ERROR] ReferenceError: r is not defined From 338b42815ccbdbb8376fd667fb94c53c4d338225 Mon Sep 17 00:00:00 2001 From: FetBoba Date: Sun, 19 Apr 2026 14:55:52 +0800 Subject: [PATCH 02/12] Implemented correct compilation for WASM-compiled mls files --- .../src/test/scala/hkmc2/CompilerTest.scala | 40 ++++-- .../src/main/scala/hkmc2/MLsCompiler.scala | 132 +++++++++++++----- .../scala/hkmc2/semantics/Elaborator.scala | 1 + .../src/test/mlscript-compile/.gitignore | 1 + .../mlscript-compile/wasm/SimpleTarget.mls | 3 + 5 files changed, 134 insertions(+), 43 deletions(-) create mode 100644 hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.mls diff --git a/hkmc2/js/src/test/scala/hkmc2/CompilerTest.scala b/hkmc2/js/src/test/scala/hkmc2/CompilerTest.scala index e8fc6a727f..15b942bcfb 100644 --- a/hkmc2/js/src/test/scala/hkmc2/CompilerTest.scala +++ b/hkmc2/js/src/test/scala/hkmc2/CompilerTest.scala @@ -8,6 +8,11 @@ import scala.scalajs.js.annotation._ import scala.scalajs.js.Dynamic.global class CompilerTest extends AnyFunSuite: + private def hasErrors(diagnostics: js.Array[js.Dynamic]): Bool = + diagnostics.exists: perFile => + val fileDiagnostics = perFile.diagnostics.asInstanceOf[js.Array[js.Dynamic]] + fileDiagnostics.exists(_.kind is "error") + private def loadStandardLibrary(): Map[String, String] = val projectRoot = node.process.cwd() val compilePath = node.path.join(projectRoot, "hkmc2", "shared", "src", "test", "mlscript-compile") @@ -57,10 +62,7 @@ class CompilerTest extends AnyFunSuite: val diagnostics = compiler.compile(inputPath) - val hasErrors = diagnostics.exists: perFile => - val fileDiagnostics = perFile.diagnostics.asInstanceOf[scala.scalajs.js.Array[scala.scalajs.js.Dynamic]] - fileDiagnostics.exists(_.kind is "error") - assert(!hasErrors, "Compilation should succeed without errors") + assert(!hasErrors(diagnostics), "Compilation should succeed without errors") val outputExists = fs.exists(Path(outputPath)) assert(outputExists, "Output JavaScript file should be generated") @@ -92,6 +94,30 @@ class CompilerTest extends AnyFunSuite: assert(fs.exists(Path("/Foo.mjs")), "First output should exist") assert(fs.exists(Path("/Bar.mjs")), "Second output should exist") + + test("compiler emits wasm artifacts for a wasm-targeted program"): + val (fs, compiler) = createCompiler() + + fs.write("/simpleWasm.mls", + """|#config(target: CompilationTarget.Wasm) + | + |40 + 2 + |""".stripMargin) + + val diagnostics = compiler.compile("/simpleWasm.mls") + + assert(!hasErrors(diagnostics), "Compilation should succeed without errors") + + assert(fs.exists(Path("/simpleWasm.mjs")), "Glue JavaScript file should be generated") + assert(fs.exists(Path("/simpleWasm.wat")), "WAT file should be generated") + + val glue = fs.read("/simpleWasm.mjs") + assert(glue.contains("binaryenCompileToModule"), "Glue code should instantiate the WAT module") + assert(glue.contains("export const __mlx_wasm"), "Glue code should expose the internal wasm loader") + assert(glue.contains("export default"), "Glue code should export the module result by default") + + val wat = fs.read("/simpleWasm.wat") + assert(wat.contains("(module"), "Generated WAT should contain a module") test("compiler can report errors"): val (fs, compiler) = createCompiler() @@ -102,8 +128,4 @@ class CompilerTest extends AnyFunSuite: assert(diagnostics.length is 1, "Should report diagnostics for only one file") - val hasErrors = diagnostics.exists: perFile => - val fileDiagnostics = perFile.diagnostics.asInstanceOf[scala.scalajs.js.Array[scala.scalajs.js.Dynamic]] - fileDiagnostics.exists(_.kind is "error") - - assert(hasErrors, "Compilation should report errors") + assert(hasErrors(diagnostics), "Compilation should report errors") diff --git a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala index 0a253b8f2e..6c238bd76d 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala @@ -65,6 +65,70 @@ class MLsCompiler var dbgParsing = false var dbgElab = false + + private def emitJs( + file: io.Path, + wd: io.Path, + program: codegen.Program, + exportedSymbol: Opt[BlockMemberSymbol], + )(using Raise, Elaborator.State, Elaborator.Ctx): Unit = + val jsb = ltl.givenIn: + codegen.js.JSBuilder() + val baseScp: utils.Scope = + utils.Scope.empty(utils.Scope.Cfg.default) + // * This line serves for `import.meta.url`, which retrieves directory and file names of mjs files. + // * Having `module id"import" with ...` in `prelude.mls` will generate `globalThis.import` that is undefined. + baseScp.addToBindings(Elaborator.State.importSymbol, "import", shadow = false) + val nestedScp = baseScp.nest + val je = nestedScp.givenIn: + jsb.program(program, exportedSymbol, wd) + cctx.fs.write(file.up / io.RelPath(s"${file.baseName}.mjs"), je.stripBreaks.mkString(100)) + + private def wasmGlue(wat: Str, compiled: codegen.wasm.text.CompiledWasmModule): Str = + s"""|import binaryen from "binaryen" + | + |const __mlx_wat = ${wat.escaped} + | + |function binaryenCompileToModule(wat, importObject) { + | const mod = binaryen.parseText(wat) + | mod.setFeatures(binaryen.Features.All) + | if (!mod.validate()) throw new Error("Generated WAT is invalid") + | const modBuf = mod.emitBinary() + | mod.dispose() + | return WebAssembly.instantiate(modBuf, importObject) + |} + | + |function __mlx_importObject() { + | return { + | system: { + | memory: new WebAssembly.Memory({ initial: ${compiled.systemMemMinPages} }) + | } + | } + |} + | + |const __mlx_wasmPromise = binaryenCompileToModule(__mlx_wat, __mlx_importObject()) + | + |export const __mlx_wasm = () => __mlx_wasmPromise + | + |export default __mlx_wasm().then(({ instance }) => instance.exports[${compiled.entryName.escaped}]()) + |""".stripMargin + + private def emitWasm( + file: io.Path, + wd: io.Path, + program: codegen.Program, + exportedSymbol: Opt[BlockMemberSymbol], + )(using Raise, Elaborator.State): Unit = + val baseScp: utils.Scope = + utils.Scope.empty(utils.Scope.Cfg.default) + val nestedScp = baseScp.nest + val watb = ltl.givenIn: + new codegen.wasm.text.WatBuilder() + val compiled = nestedScp.givenIn: + watb.program(program, exportedSymbol, wd, Nil, Set.empty) + val watStr = compiled.wat.mkString(100) + cctx.fs.write(file.up / io.RelPath(s"${file.baseName}.wat"), watStr) + cctx.fs.write(file.up / io.RelPath(s"${file.baseName}.mjs"), wasmGlue(watStr, compiled)) def compileModule(file: io.Path): Unit = @@ -97,40 +161,40 @@ class MLsCompiler case Term.Ref(sym) => sym === State.termSymbol case _ => t.subTerms.exists(findQuote) val hasQuote = findQuote(blk0) - val blk = new Term.Blk( - Import(State.runtimeSymbol, runtimeFile.toString, runtimeFile) :: - // Only import `Term.mls` when necessary. - (if hasQuote then - Import(State.termSymbol, termFile.toString, termFile) :: blk0.stats - else - blk0.stats), - blk0.res - ) - val low = ltl.givenIn: - new codegen.Lowering() - with codegen.LoweringSelSanityChecks - val jsb = ltl.givenIn: - codegen.js.JSBuilder() - val le_0 = low.program(blk) - val nme = file.baseName - val exportedSymbol = parsed.definedSymbols.find(_._1 === nme).map(_._2) - val le_1 = ltl.givenIn: - codegen.BlockSimplifier(exportedSymbol.toSet)(le_0) - val le_2 = ltl.givenIn: - codegen.DeadParamElim(le_1) - val baseScp: utils.Scope = - utils.Scope.empty(utils.Scope.Cfg.default) - // * This line serves for `import.meta.url`, which retrieves directory and file names of mjs files. - // * Having `module id"import" with ...` in `prelude.mls` will generate `globalThis.import` that is undefined. - baseScp.addToBindings(Elaborator.State.importSymbol, "import", shadow = false) - val nestedScp = baseScp.nest - val je = nestedScp.givenIn: - jsb.program(le_2, exportedSymbol, wd) - val jsStr = je.stripBreaks.mkString(100) - val out = file.up / io.RelPath(file.baseName + ".mjs") - cctx.fs.write(out, jsStr) + val effectiveCfg = blk0.stats.collect: + case sc: SetConfig => sc.modify + .foldLeft(config): (cfg, modify) => + modify(cfg) + val blk = + effectiveCfg.target match + case CompilationTarget.JS => + new Term.Blk( + Import(State.runtimeSymbol, runtimeFile.toString, runtimeFile) :: + // Only import `Term.mls` when necessary. + (if hasQuote then + Import(State.termSymbol, termFile.toString, termFile) :: blk0.stats + else + blk0.stats), + blk0.res + ) + case CompilationTarget.Wasm => + blk0 + effectiveCfg.givenIn: + val low = ltl.givenIn: + new codegen.Lowering() + with codegen.LoweringSelSanityChecks + val le_0 = low.program(blk) + val nme = file.baseName + val exportedSymbol = parsed.definedSymbols.find(_._1 === nme).map(_._2) + val le_1 = ltl.givenIn: + codegen.BlockSimplifier(exportedSymbol.toSet)(le_0) + val le_2 = ltl.givenIn: + codegen.DeadParamElim(le_1) + effectiveCfg.target match + case CompilationTarget.JS => + emitJs(file, wd, le_2, exportedSymbol) + case CompilationTarget.Wasm => + emitWasm(file, wd, le_2, exportedSymbol) end MLsCompiler - - diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala index dd85645ce8..c20e31c9d9 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala @@ -385,6 +385,7 @@ extends Importer with ucs.SplitElaborator: | Keyword.`private` )) => S(Annot.Modifier(kw)) case App(Ident("config"), Tup(args)) => + // Config target flag override is not allowed val hasTargetOverride = args.collectFirst: case InfixApp(Ident("target"), Keywrd(Keyword.`:`), _) => () .nonEmpty diff --git a/hkmc2/shared/src/test/mlscript-compile/.gitignore b/hkmc2/shared/src/test/mlscript-compile/.gitignore index 28c9ba4363..e91e7151b9 100644 --- a/hkmc2/shared/src/test/mlscript-compile/.gitignore +++ b/hkmc2/shared/src/test/mlscript-compile/.gitignore @@ -1,4 +1,5 @@ *.mjs +*.wat !/Predef.mjs !/Runtime.mjs !/RuntimeJS.mjs diff --git a/hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.mls b/hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.mls new file mode 100644 index 0000000000..76c3b1aa51 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.mls @@ -0,0 +1,3 @@ +#config(target: CompilationTarget.Wasm) + +40 + 2 From 2ba41a31e45d41d35b471c520c2ba35345cab86f Mon Sep 17 00:00:00 2001 From: FetBoba Date: Mon, 20 Apr 2026 18:49:18 +0800 Subject: [PATCH 03/12] Added full file import support --- .../scala/hkmc2/WasmModuleImportTest.scala | 81 +++++ .../src/main/scala/hkmc2/CompilerCtx.scala | 5 +- .../src/main/scala/hkmc2/MLsCompiler.scala | 341 ++++++++++++++---- .../scala/hkmc2/codegen/wasm/text/Ctx.scala | 14 +- .../hkmc2/codegen/wasm/text/WatBuilder.scala | 29 +- .../mlscript-compile/wasm/ImportValue.mls | 5 + .../mlscript-compile/wasm/ImportedValue.mls | 4 + .../src/test/mlscript/backlog/ToTriage.mls | 24 +- .../src/test/mlscript/codegen/Spreads.mls | 4 +- .../src/test/mlscript/lifter/ClassInFun.mls | 2 +- .../src/test/scala/hkmc2/WasmDiffMaker.scala | 9 +- 11 files changed, 391 insertions(+), 127 deletions(-) create mode 100644 hkmc2/jvm/src/test/scala/hkmc2/WasmModuleImportTest.scala create mode 100644 hkmc2/shared/src/test/mlscript-compile/wasm/ImportValue.mls create mode 100644 hkmc2/shared/src/test/mlscript-compile/wasm/ImportedValue.mls diff --git a/hkmc2/jvm/src/test/scala/hkmc2/WasmModuleImportTest.scala b/hkmc2/jvm/src/test/scala/hkmc2/WasmModuleImportTest.scala new file mode 100644 index 0000000000..0a340c805d --- /dev/null +++ b/hkmc2/jvm/src/test/scala/hkmc2/WasmModuleImportTest.scala @@ -0,0 +1,81 @@ +package hkmc2 + +import org.scalatest.funsuite.AnyFunSuite + +import mlscript.utils._, shorthands._ +import io.PlatformPath.given + +class WasmModuleImportTest extends AnyFunSuite: + + private val workingDir = os.pwd + private val mainTestDir = TestFolders.mainTestDir(workingDir) + private val compileDir = TestFolders.compileTestDir(workingDir) + + private def compiler(report: ReportFormatter)(using CompilerCtx, Config): MLsCompiler = + MLsCompiler( + paths = new MLsCompiler.Paths: + val preludeFile = mainTestDir / "mlscript" / "decls" / "Prelude.mls" + val runtimeFile = compileDir / "Runtime.mjs" + val termFile = compileDir / "Term.mjs" + , + mkRaise = report.mkRaise, + ) + + test("compiler wires wasm file imports through generated wat and glue"): + given CompilerCtx = CompilerCtx.fresh(io.FileSystem.default) + given Config = Config.default(mainTestDir) + + val report = ReportFormatter(_ => (), colorize = false) + val wasmDir = compileDir / "wasm" + val entryFile = wasmDir / "ImportValue.mls" + val importedModule = wasmDir / "ImportedValue.mjs" + val importedWat = wasmDir / "ImportedValue.wat" + val importedRelativePath = "./" + importedModule.relativeTo(wasmDir).toString + + val mlxCompiler = compiler(report) + if os.exists(importedModule) then os.remove(importedModule) + if os.exists(importedWat) then os.remove(importedWat) + mlxCompiler.compileModule(entryFile) + + assert(report.badLines.isEmpty, "Wasm module import fixture should compile without diagnostics") + assert(os.exists(importedModule), "Compiling an importer should emit dependency glue") + assert(os.exists(importedWat), "Compiling an importer should emit dependency WAT") + assert(os.exists(wasmDir / "ImportValue.mjs"), "Importer Wasm glue should be generated") + assert(os.exists(wasmDir / "ImportValue.wat"), "Importer WAT should be generated") + + val importerWat = os.read(wasmDir / "ImportValue.wat") + assert( + importerWat.contains(s""""${importedModule.toString}""""), + "Importer WAT should import from the generated dependency module", + ) + + val importerGlue = os.read(wasmDir / "ImportValue.mjs") + assert( + importerGlue.contains(s"""import { __mlx_wasm as __mlx_dep_0 } from "$importedRelativePath";"""), + "Importer glue should load the dependency glue module", + ) + assert( + importerGlue.contains(s"""importObject["${importedModule.toString}"] = (await __mlx_dep_0()).instance.exports"""), + "Importer glue should pass dependency exports into instantiation", + ) + + val importedModuleUrl = (wasmDir / "ImportValue.mjs").toIO.toURI.toString + val runtimeCheck = os.proc( + "node", + "--input-type=module", + "-e", + """const [url] = process.argv.slice(1); + |try { + | const mod = await import(url); + | await mod.__mlx_wasm(); + | console.log("ok"); + |} catch (err) { + | console.error(err?.stack ?? String(err)); + | process.exit(1); + |} + |""".stripMargin, + importedModuleUrl, + ).call(cwd = workingDir) + assert(runtimeCheck.out.text().trim == "ok", "Generated importer glue should instantiate successfully") + +end WasmModuleImportTest diff --git a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala index 968a6efd70..c6b135024b 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala @@ -74,8 +74,7 @@ class CompilerCtx( case cur @ S(art) => if art.lastChangedTimestamp < lastMod then mk else art - - + object CompilerCtx: inline def get(using cctx: CompilerCtx) = cctx @@ -110,5 +109,3 @@ trait CompilerCache: end CompilerCache - - diff --git a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala index 6c238bd76d..5791f2f0fa 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala @@ -6,6 +6,7 @@ import mlscript.utils.*, shorthands.* import hkmc2.io import utils.* +import hkmc2.Message.MessageContext import hkmc2.semantics.* import hkmc2.syntax.Keyword.`override` import semantics.Elaborator.{Ctx, State} @@ -52,20 +53,115 @@ class MLsCompiler (paths: MLsCompiler.Paths, mkRaise: io.Path => Raise) (using cctx: CompilerCtx, config: Config): import paths.* - - - + + /** Module metadata collected during elaboration. */ + private case class ModuleInfo( + file: io.Path, + block: Term.Blk, + exportedSymbol: Opt[BlockMemberSymbol], + effectiveCfg: Config, + hasQuote: Bool, + ) + + /** A Wasm module imported as a dependency. */ + private case class WasmDependency( + importPath: Str, + compiled: codegen.wasm.text.CompiledWasmModule, + ) + + /** Result of compiling a module to Wasm with its dependencies. */ + private case class WasmCompilation( + compiled: codegen.wasm.text.CompiledWasmModule, + dependencies: Seq[WasmDependency], + ) + // TODO adapt logic given DebugPrinter = new DebugPrinter val etl = new TraceLogger{override def doTrace: Bool = false} val ltl = new TraceLogger{override def doTrace: Bool = false} // val ltl = new TraceLogger{override def doTrace: Bool = true} val rtl = new TraceLogger{override def doTrace: Bool = false} - - + var dbgParsing = false var dbgElab = false + /** Collects metadata about an elaborated module. */ + private def moduleInfo( + file: io.Path, + parsed: syntax.Tree.Block, + block: Term.Blk, + )(using Raise, Elaborator.State, Elaborator.Ctx): ModuleInfo = + def findQuote(t: semantics.Statement): Bool = t match + case Term.Quoted(_) | Term.Unquoted(_) => true + case Term.Ref(sym) => sym === Elaborator.State.termSymbol + case _ => t.subTerms.exists(findQuote) + + val resolver = Resolver(rtl) + resolver.traverseBlock(block)(using Resolver.ICtx.empty) + ModuleInfo( + file = file, + block = block, + exportedSymbol = parsed.definedSymbols.find(_._1 === file.baseName).map(_._2), + effectiveCfg = block.stats.collect: + case sc: SetConfig => sc.modify + .foldLeft(config): (cfg, modify) => + modify(cfg), + hasQuote = findQuote(block), + ) + + /** Elaborates and collects metadata for an imported module. */ + private def elaborateImportedModule( + file: io.Path, + prelude: Elaborator.Ctx, + )(using Elaborator.State, CompilerCtx): ModuleInfo = + val parentCctx = summon[CompilerCtx] + given Raise = mkRaise(file) + val parsed = etl.givenIn: + parentCctx.getElaboratedBlock(file, prelude).tree + prelude.nestLocal("file:" + file.baseName).givenIn: + given CompilerCtx = parentCctx.derive(file) + val elab = Elaborator(etl, file.up, prelude) + val (blk, _) = elab.importFrom(parsed) + moduleInfo(file, parsed, blk) + + /** Lowers a module to intermediate representation with optimization passes. */ + private def lowerModule(module: ModuleInfo)(using Raise, Elaborator.State, Elaborator.Ctx): codegen.Program = + val blk = + module.effectiveCfg.target match + case CompilationTarget.JS => + new Term.Blk( + Import(State.runtimeSymbol, runtimeFile.toString, runtimeFile) :: + // Only import `Term.mls` when necessary. + (if module.hasQuote then + Import(State.termSymbol, termFile.toString, termFile) :: module.block.stats + else + module.block.stats), + module.block.res + ) + case CompilationTarget.Wasm => + module.block + module.effectiveCfg.givenIn: + val low = ltl.givenIn: + new codegen.Lowering() + with codegen.LoweringSelSanityChecks + val lowered = low.program(blk) + val simplified = ltl.givenIn: + codegen.BlockSimplifier(module.exportedSymbol.toSet)(lowered) + ltl.givenIn: + codegen.DeadParamElim(simplified) + + /** Resolves an import path to the source `.mls` file if possible. */ + private def resolveImportSource(importPath: Str, moduleDir: io.Path): Opt[io.Path] = + val importedPath = + if importPath.startsWith("/") + then io.Path(importPath) + else moduleDir / io.RelPath(importPath) + if importedPath.ext === "mjs" then + val resolved = importedPath.up / io.RelPath(s"${importedPath.baseName}.mls") + resolved.optionIf(cctx.fs.exists(resolved)) + else N + + /** Compiles a module to JavaScript and writes the `.mjs` file. */ private def emitJs( file: io.Path, wd: io.Path, @@ -84,10 +180,47 @@ class MLsCompiler jsb.program(program, exportedSymbol, wd) cctx.fs.write(file.up / io.RelPath(s"${file.baseName}.mjs"), je.stripBreaks.mkString(100)) - private def wasmGlue(wat: Str, compiled: codegen.wasm.text.CompiledWasmModule): Str = + /** Generates JavaScript glue code for instantiating and importing a Wasm module. */ + private def wasmGlue( + wd: io.Path, + wat: Str, + intrinsicSupportWat: Str, + compiled: codegen.wasm.text.CompiledWasmModule, + exportedSymbol: Opt[BlockMemberSymbol], + dependencies: Seq[WasmDependency], + ): Str = + def relativeImportPath(path: Str): Str = + if path.startsWith("/") + then "./" + io.Path(path).relativeTo(wd).map(_.toString).getOrElse(path) + else path + + val dependencyImports = dependencies.zipWithIndex.map: + case (dependency, idx) => + s"""import { __mlx_wasm as __mlx_dep_$idx } from "${relativeImportPath(dependency.importPath)}";""" + val dependencyImportStmts = + if dependencyImports.isEmpty then "" + else dependencyImports.mkString("", "\n", "\n\n") + val dependencyBindings = dependencies.zipWithIndex.map: + case (dependency, idx) => + s""" importObject[${dependency.importPath.escaped}] = (await __mlx_dep_$idx()).instance.exports""" + val dependencyBindingStmts = + if dependencyBindings.isEmpty then "" + else dependencyBindings.mkString("", "\n", "\n") + val exportedValueExpr = + exportedSymbol.flatMap: sym => + compiled.sessionExports.collectFirst: + case func: codegen.wasm.text.SessionFunc if func.sym === sym => + s"""instance.exports[${func.exportName.escaped}]""" + case global: codegen.wasm.text.SessionGlobal if global.sym === sym => + s"""instance.exports[${global.exportName.escaped}].value""" + case singleton: codegen.wasm.text.SessionSingleton if singleton.blockSym === sym => + s"""instance.exports[${singleton.exportName.escaped}].value""" + .getOrElse(s"""instance.exports[${compiled.entryName.escaped}]()""") s"""|import binaryen from "binaryen" + |$dependencyImportStmts | |const __mlx_wat = ${wat.escaped} + |const __mlx_intrinsics_wat = ${intrinsicSupportWat.escaped} | |function binaryenCompileToModule(wat, importObject) { | const mod = binaryen.parseText(wat) @@ -98,103 +231,155 @@ class MLsCompiler | return WebAssembly.instantiate(modBuf, importObject) |} | - |function __mlx_importObject() { + |async function __mlx_buildSystem() { + | const mem = new WebAssembly.Memory({ initial: ${compiled.systemMemMinPages} }) + | const decodeUtf16 = new TextDecoder("utf-16le") + | const intrinsicModule = await binaryenCompileToModule(__mlx_intrinsics_wat, {}) | return { - | system: { - | memory: new WebAssembly.Memory({ initial: ${compiled.systemMemMinPages} }) - | } + | mem, + | mlx_str_from_utf16: (ptr, byteLen) => + | decodeUtf16.decode(new Uint8Array(mem.buffer, ptr, byteLen)), + | ...intrinsicModule.instance.exports | } |} | - |const __mlx_wasmPromise = binaryenCompileToModule(__mlx_wat, __mlx_importObject()) + |async function __mlx_importObject() { + | const importObject = { + | system: await __mlx_buildSystem() + | } + |$dependencyBindingStmts + | return importObject + |} + | + |const __mlx_wasmPromise = __mlx_importObject() + | .then(importObject => binaryenCompileToModule(__mlx_wat, importObject)) | |export const __mlx_wasm = () => __mlx_wasmPromise | - |export default __mlx_wasm().then(({ instance }) => instance.exports[${compiled.entryName.escaped}]()) + |export default __mlx_wasm().then(({ instance }) => $exportedValueExpr) |""".stripMargin - private def emitWasm( - file: io.Path, - wd: io.Path, - program: codegen.Program, - exportedSymbol: Opt[BlockMemberSymbol], - )(using Raise, Elaborator.State): Unit = + /** Generates WAT for the intrinsics support module. */ + private def wasmIntrinsicSupportWat()(using Raise, Elaborator.State): Str = val baseScp: utils.Scope = utils.Scope.empty(utils.Scope.Cfg.default) - val nestedScp = baseScp.nest val watb = ltl.givenIn: new codegen.wasm.text.WatBuilder() - val compiled = nestedScp.givenIn: - watb.program(program, exportedSymbol, wd, Nil, Set.empty) - val watStr = compiled.wat.mkString(100) - cctx.fs.write(file.up / io.RelPath(s"${file.baseName}.wat"), watStr) - cctx.fs.write(file.up / io.RelPath(s"${file.baseName}.mjs"), wasmGlue(watStr, compiled)) - - + baseScp.nest.givenIn: + watb.intrinsicSupportModule().mkString(100) + + /** Compiles a module to Wasm and writes `.wat` and `.mjs` files. */ + private def emitWasm( + module: ModuleInfo, + prelude: Elaborator.Ctx, + memo: mutable.Map[io.Path, WasmCompilation], + )(using Raise, Elaborator.State, CompilerCtx): codegen.wasm.text.CompiledWasmModule = + val moduleDir = module.file.up + val moduleBaseName = module.file.baseName + val moduleMjsFile = moduleDir / io.RelPath(s"$moduleBaseName.mjs") + val compilation = memo.getOrElseUpdate( + module.file, + locally: + given Raise = mkRaise(module.file) + prelude.givenIn: + val program = lowerModule(module) + val dependencies = mutable.ArrayBuffer.empty[WasmDependency] + program.imports.foreach: + case (sym, importPath) => + resolveImportSource(importPath, moduleDir) match + case S(sourceFile) => + val importedModule = elaborateImportedModule(sourceFile, prelude) + if importedModule.effectiveCfg.target =/= CompilationTarget.Wasm then + val importedTarget = importedModule.effectiveCfg.target.toString + raise( + ErrorReport( + msg"Wasm modules can only import Wasm-targeted `.mls` files; " + + msg"`${sourceFile.toString}` targets `$importedTarget`" -> + sym.toLoc :: Nil, + source = Diagnostic.Source.Compilation, + ), + ) + throw new IllegalStateException(s"Expected Wasm-targeted imported module at ${sourceFile.toString}") + dependencies += WasmDependency( + importPath, + emitWasm(importedModule, prelude, memo), + ) + case N => + raise( + ErrorReport( + msg"Wasm modules currently only support imports of Wasm-targeted `.mls` files; " + + msg"`${importPath}` cannot be resolved that way" -> + sym.toLoc :: Nil, + source = Diagnostic.Source.Compilation, + ), + ) + + val baseScp: utils.Scope = + utils.Scope.empty(utils.Scope.Cfg.default) + val nestedScp = baseScp.nest + val watb = ltl.givenIn: + new codegen.wasm.text.WatBuilder() + val sessionImports: Seq[codegen.wasm.text.SessionBinding] = + val seen = mutable.LinkedHashSet.empty[Str] + dependencies.iterator + .flatMap(_.compiled.sessionExports) + .filter: binding => + seen.add(binding.bindingKey) + .toSeq + WasmCompilation( + compiled = nestedScp.givenIn: + watb.program( + program, + module.exportedSymbol, + moduleDir, + sessionImports, + module.exportedSymbol.toSet, + moduleMjsFile.toString, + ), + dependencies = dependencies.toSeq, + ) + ) + + val watStr = compilation.compiled.wat.mkString(100) + cctx.fs.write(moduleDir / io.RelPath(s"$moduleBaseName.wat"), watStr) + cctx.fs.write( + moduleMjsFile, + wasmGlue( + moduleDir, + watStr, + wasmIntrinsicSupportWat(), + compilation.compiled, + module.exportedSymbol, + compilation.dependencies, + ), + ) + + compilation.compiled + def compileModule(file: io.Path): Unit = - val wd = file.up - given Raise = mkRaise(file) - given Elaborator.State = new Elaborator.State: override def dbg: Bool = dbgElab - + val preludeParse = ParserSetup(preludeFile, dbgParsing) val mainParse = ParserSetup(file, dbgParsing) - + val elab = Elaborator(etl, wd, Ctx.empty) - val initState = State.init.nestLocal("prelude") - - val (pblk, newCtx) = elab.importFrom(preludeParse.resultBlk)(using initState) - + val (_, newCtx) = elab.importFrom(preludeParse.resultBlk)(using initState) + newCtx.nestLocal("file:"+file.baseName).givenIn: given CompilerCtx = cctx.derive(file) val elab = Elaborator(etl, wd, newCtx) val parsed = mainParse.resultBlk val (blk0, _) = elab.importFrom(parsed) - val resolver = Resolver(rtl) - resolver.traverseBlock(blk0)(using Resolver.ICtx.empty) - def findQuote(t: semantics.Statement): Bool = t match - case Term.Quoted(_) | Term.Unquoted(_) => true - case Term.Ref(sym) => sym === State.termSymbol - case _ => t.subTerms.exists(findQuote) - val hasQuote = findQuote(blk0) - val effectiveCfg = blk0.stats.collect: - case sc: SetConfig => sc.modify - .foldLeft(config): (cfg, modify) => - modify(cfg) - val blk = - effectiveCfg.target match - case CompilationTarget.JS => - new Term.Blk( - Import(State.runtimeSymbol, runtimeFile.toString, runtimeFile) :: - // Only import `Term.mls` when necessary. - (if hasQuote then - Import(State.termSymbol, termFile.toString, termFile) :: blk0.stats - else - blk0.stats), - blk0.res - ) - case CompilationTarget.Wasm => - blk0 - effectiveCfg.givenIn: - val low = ltl.givenIn: - new codegen.Lowering() - with codegen.LoweringSelSanityChecks - val le_0 = low.program(blk) - val nme = file.baseName - val exportedSymbol = parsed.definedSymbols.find(_._1 === nme).map(_._2) - val le_1 = ltl.givenIn: - codegen.BlockSimplifier(exportedSymbol.toSet)(le_0) - val le_2 = ltl.givenIn: - codegen.DeadParamElim(le_1) - effectiveCfg.target match - case CompilationTarget.JS => - emitJs(file, wd, le_2, exportedSymbol) - case CompilationTarget.Wasm => - emitWasm(file, wd, le_2, exportedSymbol) - - + val module = moduleInfo(file, parsed, blk0) + module.effectiveCfg.target match + case CompilationTarget.JS => + given Raise = mkRaise(file) + emitJs(file, wd, lowerModule(module), module.exportedSymbol) + case CompilationTarget.Wasm => + emitWasm(module, newCtx, mutable.Map.empty) + end MLsCompiler diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/Ctx.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/Ctx.scala index 188aeb3582..ffa95b0c91 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/Ctx.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/Ctx.scala @@ -136,14 +136,13 @@ final case class CompiledWasmModule( sessionExports: Seq[SessionBinding], ) -/** Context used while collecting REPL/session exports for a single Wasm module. +/** Context for collecting session exports from a Wasm module. * - * @param symbolsToExport - * The symbols from the current module that should be recorded as session exports. - * @param collectedBindings - * The session bindings accumulated while compiling the current module. + * @param moduleName + * The module name used for identifying exports in dependent modules. */ final class SessionExportCtx( + val moduleName: Str, val symbolsToExport: Set[Local], val collectedBindings: ArrayBuf[SessionBinding], ): @@ -153,15 +152,16 @@ final class SessionExportCtx( collectedBindings += binding def freshCollector(): SessionExportCtx = - SessionExportCtx(symbolsToExport, ArrayBuf.empty) + SessionExportCtx(moduleName, symbolsToExport, ArrayBuf.empty) end SessionExportCtx object SessionExportCtx: def apply( + moduleName: Str, symbolsToExport: Set[Local], collectedBindings: ArrayBuf[SessionBinding], ): SessionExportCtx = - new SessionExportCtx(symbolsToExport, collectedBindings) + new SessionExportCtx(moduleName, symbolsToExport, collectedBindings) /** A Wasm function and its associated information. * diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala index f6c5653852..be31b68b67 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala @@ -132,7 +132,7 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: summon[SessionExportCtx].emit(SessionSingleton( blockSym = unitDefn.sym, objectSym = singletonOwner, - moduleName = SessionBinding.ReplModuleName, + moduleName = summon[SessionExportCtx].moduleName, exportName = singletonInfo.globalName, globalTy = singletonInfo.globalTy, )) @@ -447,7 +447,7 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: ) summon[SessionExportCtx].emit(SessionGlobal( sym = sym, - moduleName = SessionBinding.ReplModuleName, + moduleName = summon[SessionExportCtx].moduleName, exportName = exportName, globalType = GlobalType(RefType.anyref, mutable = true), )) @@ -1517,7 +1517,7 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: if summon[SessionExportCtx].shouldExport(defn.sym) then summon[SessionExportCtx].emit(SessionFunc( sym = defn.sym, - moduleName = SessionBinding.ReplModuleName, + moduleName = summon[SessionExportCtx].moduleName, exportName = sym.nme, funcType = FunctionType(funcInfo.getSignatureType), )) @@ -1647,7 +1647,7 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: if !isSingletonObj && clsLikeDefn.sym.nameIsMeaningful then summon[SessionExportCtx].emit(SessionFunc( sym = clsLikeDefn.sym, - moduleName = SessionBinding.ReplModuleName, + moduleName = summon[SessionExportCtx].moduleName, exportName = clsLikeDefn.sym.nme, funcType = FunctionType( SignatureType( @@ -1667,7 +1667,7 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: summon[SessionExportCtx].emit(SessionSingleton( blockSym = clsLikeDefn.sym, objectSym = singletonOwner, - moduleName = SessionBinding.ReplModuleName, + moduleName = summon[SessionExportCtx].moduleName, exportName = info.globalName, globalTy = info.globalTy, )) @@ -2002,25 +2002,10 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: wd: io.Path, sessionImports: Seq[SessionBinding], preservedSessionSymbols: Set[Local], + currentModuleName: Str, )(using Raise, Scope): CompiledWasmModule = - for imprt <- p.imports do - raise( - ErrorReport( - msg"Import of symbol `${imprt._2}` not implemented yet" -> imprt._1.toLoc :: Nil, - extraInfo = S(imprt), - source = Diagnostic.Source.Compilation, - ), - ) - exprt.foreach: exprt => - raise( - ErrorReport( - msg"Export of symbol `${exprt.nme}` not implemented yet" -> exprt.toLoc :: Nil, - extraInfo = S(exprt), - source = Diagnostic.Source.Compilation, - ), - ) - val sessionExportCtx = SessionExportCtx( + moduleName = currentModuleName, symbolsToExport = preservedSessionSymbols, collectedBindings = ArrayBuf.empty, ) diff --git a/hkmc2/shared/src/test/mlscript-compile/wasm/ImportValue.mls b/hkmc2/shared/src/test/mlscript-compile/wasm/ImportValue.mls new file mode 100644 index 0000000000..02b4b9d2d9 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-compile/wasm/ImportValue.mls @@ -0,0 +1,5 @@ +#config(target: CompilationTarget.Wasm) + +import "./ImportedValue.mls" + +ImportedValue.base + 1 diff --git a/hkmc2/shared/src/test/mlscript-compile/wasm/ImportedValue.mls b/hkmc2/shared/src/test/mlscript-compile/wasm/ImportedValue.mls new file mode 100644 index 0000000000..05eed34c67 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-compile/wasm/ImportedValue.mls @@ -0,0 +1,4 @@ +#config(target: CompilationTarget.Wasm) + +object ImportedValue with + val base = 41 diff --git a/hkmc2/shared/src/test/mlscript/backlog/ToTriage.mls b/hkmc2/shared/src/test/mlscript/backlog/ToTriage.mls index f48014eaec..6c2fa3d21a 100644 --- a/hkmc2/shared/src/test/mlscript/backlog/ToTriage.mls +++ b/hkmc2/shared/src/test/mlscript/backlog/ToTriage.mls @@ -233,7 +233,7 @@ id(Foo)(1) // ——— ——— ——— data class FooSpd(...args) -//│ ╔══[INTERNAL ERROR] Compiler reached an unexpected state at 'Elaborator.scala:1322': mismatched param list lengths List() vs List(term:FooSpd/args) +//│ ╔══[INTERNAL ERROR] Compiler reached an unexpected state at 'Elaborator.scala:1323': mismatched param list lengths List() vs List(term:FooSpd/args) //│ ╟── The compilation result may be incorrect. //│ ╙── This is a compiler bug; please report it to the maintainers. @@ -243,25 +243,25 @@ FooSpd(1, 2, 3).args :todo if FooSpd(1, 2, 3) is FooSpd(...args) then args //│ ╔══[COMPILATION ERROR] Unrecognized pattern (spread). -//│ ║ l.249: if FooSpd(1, 2, 3) is FooSpd(...args) then args +//│ ║ l.244: if FooSpd(1, 2, 3) is FooSpd(...args) then args //│ ╙── ^^^^^^^ //│ ╔══[COMPILATION ERROR] Name not found: args -//│ ║ l.249: if FooSpd(1, 2, 3) is FooSpd(...args) then args +//│ ║ l.244: if FooSpd(1, 2, 3) is FooSpd(...args) then args //│ ╙── ^^^^ //│ ╔══[COMPILATION ERROR] The constructor does not take any arguments but found one argument. -//│ ║ l.249: if FooSpd(1, 2, 3) is FooSpd(...args) then args +//│ ║ l.244: if FooSpd(1, 2, 3) is FooSpd(...args) then args //│ ╙── ^^^^^^ //│ ═══[RUNTIME ERROR] Error: match error if FooSpd(1, 2, 3) is FooSpd(a, b, c) then [a, b, c] //│ ╔══[COMPILATION ERROR] The constructor does not take any arguments but found three arguments. -//│ ║ l.261: if FooSpd(1, 2, 3) is FooSpd(a, b, c) then [a, b, c] +//│ ║ l.256: if FooSpd(1, 2, 3) is FooSpd(a, b, c) then [a, b, c] //│ ╙── ^^^^^^^ //│ ═══[RUNTIME ERROR] Error: match error if FooSpd(1, 2, 3) is FooSpd(arg) then arg //│ ╔══[COMPILATION ERROR] The constructor does not take any arguments but found one argument. -//│ ║ l.267: if FooSpd(1, 2, 3) is FooSpd(arg) then arg +//│ ║ l.262: if FooSpd(1, 2, 3) is FooSpd(arg) then arg //│ ╙── ^^^ //│ ═══[RUNTIME ERROR] Error: match error @@ -289,10 +289,10 @@ fun foo() = let bar(x: Str): Str = x + x bar("test") //│ ╔══[COMPILATION ERROR] Unsupported let binding shape -//│ ║ l.294: let bar(x: Str): Str = x + x +//│ ║ l.289: let bar(x: Str): Str = x + x //│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^ //│ ╔══[COMPILATION ERROR] Name not found: bar -//│ ║ l.295: bar("test") +//│ ║ l.290: bar("test") //│ ╙── ^^^ // ——— ——— ——— @@ -306,7 +306,7 @@ module Foo(x) with print(x) //│ ╔══[COMPILATION ERROR] No definition found in scope for member 'x' //│ ╟── which references the symbol introduced here -//│ ║ l.310: module Foo(x) with +//│ ║ l.305: module Foo(x) with //│ ╙── ^ //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— //│ let Foo5; @@ -330,7 +330,7 @@ module Foo(x) with module Foo(val x) //│ ╔══[COMPILATION ERROR] No definition found in scope for member 'x' //│ ╟── which references the symbol introduced here -//│ ║ l.335: module Foo(val x) +//│ ║ l.330: module Foo(val x) //│ ╙── ^ //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— //│ let Foo7; @@ -351,7 +351,7 @@ module Foo(val x) // TODO support syntax? data class Foo(mut x) //│ ╔══[COMPILATION ERROR] Expected a valid parameter, found 'mut'-modified identifier -//│ ║ l.357: data class Foo(mut x) +//│ ║ l.352: data class Foo(mut x) //│ ╙── ^ // ——— ——— ——— @@ -366,7 +366,7 @@ class Foo :e class Id(name: UnboundName) //│ ╔══[COMPILATION ERROR] Name not found: UnboundName -//│ ║ l.372: class Id(name: UnboundName) +//│ ║ l.367: class Id(name: UnboundName) //│ ╙── ^^^^^^^^^^^ //│ ═══[COMPILATION ERROR] Expected a type, got a non-type ‹error› //│ ═══[COMPILATION ERROR] Expected a type, got ‹error› diff --git a/hkmc2/shared/src/test/mlscript/codegen/Spreads.mls b/hkmc2/shared/src/test/mlscript/codegen/Spreads.mls index f3af5f390b..d3badd45ba 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/Spreads.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/Spreads.mls @@ -58,7 +58,7 @@ foo(0, ...a) data class A(...r) let x = new A(1, 2, 3) x.r -//│ ╔══[INTERNAL ERROR] Compiler reached an unexpected state at 'Elaborator.scala:1322': mismatched param list lengths List() vs List(term:A/r) +//│ ╔══[INTERNAL ERROR] Compiler reached an unexpected state at 'Elaborator.scala:1323': mismatched param list lengths List() vs List(term:A/r) //│ ╟── The compilation result may be incorrect. //│ ╙── This is a compiler bug; please report it to the maintainers. //│ ═══[RUNTIME ERROR] Error: Access to required field 'r' yielded 'undefined' @@ -70,7 +70,7 @@ x.r data class A(...r) with fun getR = r new A(1, 2, 3).getR -//│ ╔══[INTERNAL ERROR] Compiler reached an unexpected state at 'Elaborator.scala:1322': mismatched param list lengths List() vs List(term:A/r) +//│ ╔══[INTERNAL ERROR] Compiler reached an unexpected state at 'Elaborator.scala:1323': mismatched param list lengths List() vs List(term:A/r) //│ ╟── The compilation result may be incorrect. //│ ╙── This is a compiler bug; please report it to the maintainers. //│ ═══[RUNTIME ERROR] ReferenceError: r is not defined diff --git a/hkmc2/shared/src/test/mlscript/lifter/ClassInFun.mls b/hkmc2/shared/src/test/mlscript/lifter/ClassInFun.mls index 6947c5c302..6e3f8fa51d 100644 --- a/hkmc2/shared/src/test/mlscript/lifter/ClassInFun.mls +++ b/hkmc2/shared/src/test/mlscript/lifter/ClassInFun.mls @@ -288,7 +288,7 @@ fun f(x) = let res = f(0) res.a res.b -//│ ╔══[INTERNAL ERROR] Compiler reached an unexpected state at 'Elaborator.scala:1322': mismatched param list lengths List() vs List(term:A/r) +//│ ╔══[INTERNAL ERROR] Compiler reached an unexpected state at 'Elaborator.scala:1323': mismatched param list lengths List() vs List(term:A/r) //│ ╟── The compilation result may be incorrect. //│ ╙── This is a compiler bug; please report it to the maintainers. //│ ═══[RUNTIME ERROR] ReferenceError: r is not defined diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/WasmDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/WasmDiffMaker.scala index 31ebc3c380..1da2f09460 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/WasmDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/WasmDiffMaker.scala @@ -104,7 +104,14 @@ abstract class WasmDiffMaker extends LlirDiffMaker: sessionImports.update(bindingKey, binding) val CompiledWasmModule(modWat, mainFnNme, systemMemMinPages, sessionExports) = ltl.givenIn: baseScp.nest.givenIn: - WatBuilder().program(pgrm, N, wd, sessionImports.values.toSeq, symbolsToPreserve) + WatBuilder().program( + pgrm, + N, + wd, + sessionImports.values.toSeq, + symbolsToPreserve, + SessionBinding.ReplModuleName, + ) val modWatJsLit = JSBuilder.makeStringLiteral(modWat.mkString(output.ColWidth)) if wat.isSet then From 4b907aa6b1f2fa2e2b6b13689515d2e41e83a058 Mon Sep 17 00:00:00 2001 From: FetBoba Date: Tue, 21 Apr 2026 01:53:42 +0800 Subject: [PATCH 04/12] Restored whitespaces --- hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala | 3 +++ hkmc2/shared/src/main/scala/hkmc2/Config.scala | 1 + hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala | 7 +++++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala index c6b135024b..660662c7ef 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala @@ -75,6 +75,7 @@ class CompilerCtx( if art.lastChangedTimestamp < lastMod then mk else art + object CompilerCtx: inline def get(using cctx: CompilerCtx) = cctx @@ -109,3 +110,5 @@ trait CompilerCache: end CompilerCache + + diff --git a/hkmc2/shared/src/main/scala/hkmc2/Config.scala b/hkmc2/shared/src/main/scala/hkmc2/Config.scala index f52252feef..e19d2ea0d9 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/Config.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/Config.scala @@ -341,3 +341,4 @@ object ConfigParser: source = Diagnostic.Source.Compilation)) identity end ConfigParser + diff --git a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala index 5791f2f0fa..b890338bcc 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala @@ -357,16 +357,19 @@ class MLsCompiler compilation.compiled def compileModule(file: io.Path): Unit = + val wd = file.up + given Raise = mkRaise(file) + given Elaborator.State = new Elaborator.State: override def dbg: Bool = dbgElab - val preludeParse = ParserSetup(preludeFile, dbgParsing) val mainParse = ParserSetup(file, dbgParsing) - val elab = Elaborator(etl, wd, Ctx.empty) + val initState = State.init.nestLocal("prelude") + val (_, newCtx) = elab.importFrom(preludeParse.resultBlk)(using initState) newCtx.nestLocal("file:"+file.baseName).givenIn: From e3f347e1b2e199c51899fdf47a31da034c3194b2 Mon Sep 17 00:00:00 2001 From: FetBoba Date: Tue, 21 Apr 2026 04:17:35 +0800 Subject: [PATCH 05/12] Removed unnecessary line and indentaion changes --- .../src/main/scala/hkmc2/CompilerCtx.scala | 4 ++-- .../src/main/scala/hkmc2/MLsCompiler.scala | 12 +++++----- .../src/test/mlscript/backlog/ToTriage.mls | 22 +++++++++---------- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala index 660662c7ef..968a6efd70 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala @@ -74,8 +74,8 @@ class CompilerCtx( case cur @ S(art) => if art.lastChangedTimestamp < lastMod then mk else art - - + + object CompilerCtx: inline def get(using cctx: CompilerCtx) = cctx diff --git a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala index b890338bcc..c01b924b79 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala @@ -357,21 +357,23 @@ class MLsCompiler compilation.compiled def compileModule(file: io.Path): Unit = - + val wd = file.up - + given Raise = mkRaise(file) - + given Elaborator.State = new Elaborator.State: override def dbg: Bool = dbgElab + val preludeParse = ParserSetup(preludeFile, dbgParsing) val mainParse = ParserSetup(file, dbgParsing) + val elab = Elaborator(etl, wd, Ctx.empty) - + val initState = State.init.nestLocal("prelude") val (_, newCtx) = elab.importFrom(preludeParse.resultBlk)(using initState) - + newCtx.nestLocal("file:"+file.baseName).givenIn: given CompilerCtx = cctx.derive(file) val elab = Elaborator(etl, wd, newCtx) diff --git a/hkmc2/shared/src/test/mlscript/backlog/ToTriage.mls b/hkmc2/shared/src/test/mlscript/backlog/ToTriage.mls index 981db6d159..6888b864d1 100644 --- a/hkmc2/shared/src/test/mlscript/backlog/ToTriage.mls +++ b/hkmc2/shared/src/test/mlscript/backlog/ToTriage.mls @@ -246,13 +246,13 @@ FooSpd(1, 2, 3).args :todo if FooSpd(1, 2, 3) is FooSpd(...args) then args //│ ╔══[COMPILATION ERROR] Unrecognized pattern (spread). -//│ ║ l.252: if FooSpd(1, 2, 3) is FooSpd(...args) then args +//│ ║ l.247: if FooSpd(1, 2, 3) is FooSpd(...args) then args //│ ╙── ^^^^^^^ //│ ╔══[COMPILATION ERROR] Name not found: args -//│ ║ l.252: if FooSpd(1, 2, 3) is FooSpd(...args) then args +//│ ║ l.247: if FooSpd(1, 2, 3) is FooSpd(...args) then args //│ ╙── ^^^^ //│ ╔══[COMPILATION ERROR] The constructor does not take any arguments but found one argument. -//│ ║ l.252: if FooSpd(1, 2, 3) is FooSpd(...args) then args +//│ ║ l.247: if FooSpd(1, 2, 3) is FooSpd(...args) then args //│ ╙── ^^^^^^ //│ ╔══[INTERNAL ERROR] Compiler reached an unsupported state: mismatched ctor arg and cls param sizes //│ ╟── The compilation result may be incorrect. @@ -261,7 +261,7 @@ if FooSpd(1, 2, 3) is FooSpd(...args) then args if FooSpd(1, 2, 3) is FooSpd(a, b, c) then [a, b, c] //│ ╔══[COMPILATION ERROR] The constructor does not take any arguments but found three arguments. -//│ ║ l.267: if FooSpd(1, 2, 3) is FooSpd(a, b, c) then [a, b, c] +//│ ║ l.262: if FooSpd(1, 2, 3) is FooSpd(a, b, c) then [a, b, c] //│ ╙── ^^^^^^^ //│ ╔══[INTERNAL ERROR] Compiler reached an unsupported state: mismatched ctor arg and cls param sizes //│ ╟── The compilation result may be incorrect. @@ -270,7 +270,7 @@ if FooSpd(1, 2, 3) is FooSpd(a, b, c) then [a, b, c] if FooSpd(1, 2, 3) is FooSpd(arg) then arg //│ ╔══[COMPILATION ERROR] The constructor does not take any arguments but found one argument. -//│ ║ l.276: if FooSpd(1, 2, 3) is FooSpd(arg) then arg +//│ ║ l.271: if FooSpd(1, 2, 3) is FooSpd(arg) then arg //│ ╙── ^^^ //│ ╔══[INTERNAL ERROR] Compiler reached an unsupported state: mismatched ctor arg and cls param sizes //│ ╟── The compilation result may be incorrect. @@ -301,10 +301,10 @@ fun foo() = let bar(x: Str): Str = x + x bar("test") //│ ╔══[COMPILATION ERROR] Unsupported let binding shape -//│ ║ l.306: let bar(x: Str): Str = x + x +//│ ║ l.301: let bar(x: Str): Str = x + x //│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^ //│ ╔══[COMPILATION ERROR] Name not found: bar -//│ ║ l.307: bar("test") +//│ ║ l.302: bar("test") //│ ╙── ^^^ // ——— ——— ——— @@ -318,7 +318,7 @@ module Foo(x) with print(x) //│ ╔══[COMPILATION ERROR] No definition found in scope for member 'x' //│ ╟── which references the symbol introduced here -//│ ║ l.322: module Foo(x) with +//│ ║ l.317: module Foo(x) with //│ ╙── ^ //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— //│ let Foo5; @@ -342,7 +342,7 @@ module Foo(x) with module Foo(val x) //│ ╔══[COMPILATION ERROR] No definition found in scope for member 'x' //│ ╟── which references the symbol introduced here -//│ ║ l.347: module Foo(val x) +//│ ║ l.342: module Foo(val x) //│ ╙── ^ //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— //│ let Foo7; @@ -363,7 +363,7 @@ module Foo(val x) // TODO support syntax? data class Foo(mut x) //│ ╔══[COMPILATION ERROR] Expected a valid parameter, found 'mut'-modified identifier -//│ ║ l.369: data class Foo(mut x) +//│ ║ l.364: data class Foo(mut x) //│ ╙── ^ // ——— ——— ——— @@ -378,7 +378,7 @@ class Foo :e class Id(name: UnboundName) //│ ╔══[COMPILATION ERROR] Name not found: UnboundName -//│ ║ l.384: class Id(name: UnboundName) +//│ ║ l.379: class Id(name: UnboundName) //│ ╙── ^^^^^^^^^^^ //│ ═══[COMPILATION ERROR] Expected a type, got a non-type ‹error› //│ ═══[COMPILATION ERROR] Expected a type, got ‹error› From 8b04ce473e7d5136293de3c8753d9987169be177 Mon Sep 17 00:00:00 2001 From: FetBoba Date: Wed, 22 Apr 2026 04:50:50 +0800 Subject: [PATCH 06/12] Moved import test to diff tests --- .../scala/hkmc2/WasmModuleImportTest.scala | 81 ------------------- .../src/main/scala/hkmc2/MLsCompiler.scala | 9 +-- .../hkmc2/codegen/wasm/text/WatBuilder.scala | 2 +- .../src/test/mlscript-compile/.gitignore | 1 - .../src/test/mlscript-compile/wasm/IVal.mls | 4 - .../mlscript-compile/wasm/ImportValue.mls | 5 -- .../mlscript-compile/wasm/SimpleTarget.wat | 12 +++ .../src/test/mlscript/codegen/ImportMLsJS.mls | 5 ++ 8 files changed, 22 insertions(+), 97 deletions(-) delete mode 100644 hkmc2/jvm/src/test/scala/hkmc2/WasmModuleImportTest.scala delete mode 100644 hkmc2/shared/src/test/mlscript-compile/wasm/IVal.mls delete mode 100644 hkmc2/shared/src/test/mlscript-compile/wasm/ImportValue.mls create mode 100644 hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.wat diff --git a/hkmc2/jvm/src/test/scala/hkmc2/WasmModuleImportTest.scala b/hkmc2/jvm/src/test/scala/hkmc2/WasmModuleImportTest.scala deleted file mode 100644 index 29d2693b62..0000000000 --- a/hkmc2/jvm/src/test/scala/hkmc2/WasmModuleImportTest.scala +++ /dev/null @@ -1,81 +0,0 @@ -package hkmc2 - -import org.scalatest.funsuite.AnyFunSuite - -import mlscript.utils._, shorthands._ -import io.PlatformPath.given - -class WasmModuleImportTest extends AnyFunSuite: - - private val workingDir = os.pwd - private val mainTestDir = TestFolders.mainTestDir(workingDir) - private val compileDir = TestFolders.compileTestDir(workingDir) - - private def compiler(report: ReportFormatter)(using CompilerCtx, Config): MLsCompiler = - MLsCompiler( - paths = new MLsCompiler.Paths: - val preludeFile = mainTestDir / "mlscript" / "decls" / "Prelude.mls" - val runtimeFile = compileDir / "Runtime.mjs" - val termFile = compileDir / "Term.mjs" - , - mkRaise = report.mkRaise, - ) - - test("compiler wires wasm file imports through generated wat and glue"): - given CompilerCtx = CompilerCtx.fresh(io.FileSystem.default) - given Config = Config.default(mainTestDir) - - val report = ReportFormatter(_ => (), colorize = false) - val wasmDir = compileDir / "wasm" - val entryFile = wasmDir / "ImportValue.mls" - val importedModule = wasmDir / "IVal.mjs" - val importedWat = wasmDir / "IVal.wat" - val importedRelativePath = "./" + importedModule.relativeTo(wasmDir).toString - - val mlxCompiler = compiler(report) - if os.exists(importedModule) then os.remove(importedModule) - if os.exists(importedWat) then os.remove(importedWat) - mlxCompiler.compileModule(entryFile) - - assert(report.badLines.isEmpty, "Wasm module import fixture should compile without diagnostics") - assert(os.exists(importedModule), "Compiling an importer should emit dependency glue") - assert(os.exists(importedWat), "Compiling an importer should emit dependency WAT") - assert(os.exists(wasmDir / "ImportValue.mjs"), "Importer Wasm glue should be generated") - assert(os.exists(wasmDir / "ImportValue.wat"), "Importer WAT should be generated") - - val importerWat = os.read(wasmDir / "ImportValue.wat") - assert( - importerWat.contains(s""""${importedModule.toString}""""), - "Importer WAT should import from the generated dependency module", - ) - - val importerGlue = os.read(wasmDir / "ImportValue.mjs") - assert( - importerGlue.contains(s"""import { __mlx_wasm as __mlx_dep_0 } from "$importedRelativePath";"""), - "Importer glue should load the dependency glue module", - ) - assert( - importerGlue.contains(s"""importObject["${importedModule.toString}"] = (await __mlx_dep_0()).instance.exports"""), - "Importer glue should pass dependency exports into instantiation", - ) - - val importedModuleUrl = (wasmDir / "ImportValue.mjs").toIO.toURI.toString - val runtimeCheck = os.proc( - "node", - "--input-type=module", - "-e", - """const [url] = process.argv.slice(1); - |try { - | const mod = await import(url); - | await mod.__mlx_wasm(); - | console.log("ok"); - |} catch (err) { - | console.error(err?.stack ?? String(err)); - | process.exit(1); - |} - |""".stripMargin, - importedModuleUrl, - ).call(cwd = workingDir) - assert(runtimeCheck.out.text().trim == "ok", "Generated importer glue should instantiate successfully") - -end WasmModuleImportTest diff --git a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala index c01b924b79..1bc8b2105b 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala @@ -82,6 +82,7 @@ class MLsCompiler // val ltl = new TraceLogger{override def doTrace: Bool = true} val rtl = new TraceLogger{override def doTrace: Bool = false} + var dbgParsing = false var dbgElab = false @@ -116,13 +117,11 @@ class MLsCompiler )(using Elaborator.State, CompilerCtx): ModuleInfo = val parentCctx = summon[CompilerCtx] given Raise = mkRaise(file) - val parsed = etl.givenIn: - parentCctx.getElaboratedBlock(file, prelude).tree + val artifact = etl.givenIn: + parentCctx.getElaboratedBlock(file, prelude) prelude.nestLocal("file:" + file.baseName).givenIn: given CompilerCtx = parentCctx.derive(file) - val elab = Elaborator(etl, file.up, prelude) - val (blk, _) = elab.importFrom(parsed) - moduleInfo(file, parsed, blk) + moduleInfo(file, artifact.tree, artifact.term) /** Lowers a module to intermediate representation with optimization passes. */ private def lowerModule(module: ModuleInfo)(using Raise, Elaborator.State, Elaborator.Ctx): codegen.Program = diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala index 652facad29..335a8d61ea 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala @@ -910,7 +910,7 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: symToField.collectFirst: case (fieldSym, field) if fieldSym.toString == symQualName => field.id .getOrElse: - val availableFields = symToField.keys.map(_.toString).mkString(", ") + val availableFields = symToField.keys.iterator.map(_.toString).toSeq.sorted.mkString(", ") lastWords( s"Missing field `${sym.toString}` in struct `${thisSym.toString}` with type `${structInfo.toWat.mkString()}`. Available fields: $availableFields", ) diff --git a/hkmc2/shared/src/test/mlscript-compile/.gitignore b/hkmc2/shared/src/test/mlscript-compile/.gitignore index e91e7151b9..28c9ba4363 100644 --- a/hkmc2/shared/src/test/mlscript-compile/.gitignore +++ b/hkmc2/shared/src/test/mlscript-compile/.gitignore @@ -1,5 +1,4 @@ *.mjs -*.wat !/Predef.mjs !/Runtime.mjs !/RuntimeJS.mjs diff --git a/hkmc2/shared/src/test/mlscript-compile/wasm/IVal.mls b/hkmc2/shared/src/test/mlscript-compile/wasm/IVal.mls deleted file mode 100644 index d14dc27c41..0000000000 --- a/hkmc2/shared/src/test/mlscript-compile/wasm/IVal.mls +++ /dev/null @@ -1,4 +0,0 @@ -#config(target: CompilationTarget.Wasm) - -object IVal with - val base = 41 diff --git a/hkmc2/shared/src/test/mlscript-compile/wasm/ImportValue.mls b/hkmc2/shared/src/test/mlscript-compile/wasm/ImportValue.mls deleted file mode 100644 index 4461f5dd4f..0000000000 --- a/hkmc2/shared/src/test/mlscript-compile/wasm/ImportValue.mls +++ /dev/null @@ -1,5 +0,0 @@ -#config(target: CompilationTarget.Wasm) - -import "./IVal.mls" - -IVal.base + 1 diff --git a/hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.wat b/hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.wat new file mode 100644 index 0000000000..a5b6a44b71 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.wat @@ -0,0 +1,12 @@ +(module + (type $Object (sub (struct (field $$tag (mut i32))))) + (type $plus_impl (func (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any)))) + (type $entry1 (func (result (ref null any)))) + (import "system" "plus_impl" (func $plus_impl (type $plus_impl))) + (func $entry (export "entry") (type $entry1) (result (ref null any)) + (call $plus_impl + (ref.i31 + (i32.const 40)) + (ref.i31 + (i32.const 2)))) + (elem $entry declare func $entry)) \ No newline at end of file diff --git a/hkmc2/shared/src/test/mlscript/codegen/ImportMLsJS.mls b/hkmc2/shared/src/test/mlscript/codegen/ImportMLsJS.mls index 8adf693f50..f8328e5316 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/ImportMLsJS.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/ImportMLsJS.mls @@ -53,3 +53,8 @@ new! Option.Some(1) //│ = Some(1) +import "../../mlscript-compile/wasm/SimpleTarget.mjs" +//│ SimpleTarget = 42 + +SimpleTarget + 1 +//│ = 43 From c33f4e91bdc87e63d30f425590c7bf0edd5a568c Mon Sep 17 00:00:00 2001 From: FetBoba Date: Wed, 22 Apr 2026 17:34:20 +0800 Subject: [PATCH 07/12] Updated import tests --- .../hkmc2/codegen/wasm/text/WatBuilder.scala | 11 ++- .../src/test/mlscript-compile/.gitignore | 1 + .../mlscript-compile/wasm/ImportValue.mls | 7 ++ .../mlscript-compile/wasm/SimpleTarget.mjs | 41 +++++++++++ .../mlscript-compile/wasm/SimpleTarget.mls | 4 +- .../mlscript-compile/wasm/SimpleTarget.wat | 73 +++++++++++++++++-- .../src/test/mlscript/codegen/ImportMLsJS.mls | 7 +- 7 files changed, 127 insertions(+), 17 deletions(-) create mode 100644 hkmc2/shared/src/test/mlscript-compile/wasm/ImportValue.mls create mode 100644 hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.mjs diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala index 6a1784160e..51b89645d6 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala @@ -72,7 +72,7 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: && defn.auxParams.isEmpty && (!(defn.k is syntax.Obj) || defn.parentPath.isEmpty) && (!(defn.k is syntax.Obj) || defn.methods.isEmpty) - && defn.companion.isEmpty + && defn.companion.forall(_.methods.isEmpty) /** Returns singleton metadata when `sym` resolves to a registered singleton object. */ private def singletonInfoFor(sym: Local)(using Ctx): Opt[SingletonInfo] = @@ -902,7 +902,7 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: val symToField = structInfo.compType match case ty: StructType => ty.fieldsBySym case _ => lastWords(s"Cannot select field from non-struct type: ${structInfo.compType.toWat.mkString()}") - + // Try direct lookup first val fieldIdx = symToField.get(sym).map(_.id).getOrElse: // If direct lookup fails, try matching by qualified name @@ -1568,8 +1568,11 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: break(errUnimplExpr("parentPath.nonEmpty for object")) if isSingletonObj && clsLikeDefn.methods.nonEmpty then break(errUnimplExpr("methods.nonEmpty for object")) + // TODO: Support companion object methods (currently only companion fields are allowed) if clsLikeDefn.companion.isDefined then - break(errUnimplExpr("companion.isDefined")) + val companion = clsLikeDefn.companion.get + if companion.methods.nonEmpty then + break(errUnimplExpr("companion.methods.nonEmpty")) val ctorAuxParams = clsLikeDefn.auxParams.map: ps => ps.params.map: p => @@ -1799,7 +1802,7 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: case LabelTarget(breakLabel, continueLabel) => val bodyExpr = returningTerm(body) val bodyStmt = asStatement(bodyExpr) - + if loop then Instructions.block( label = S(breakLabel), diff --git a/hkmc2/shared/src/test/mlscript-compile/.gitignore b/hkmc2/shared/src/test/mlscript-compile/.gitignore index 28c9ba4363..e91e7151b9 100644 --- a/hkmc2/shared/src/test/mlscript-compile/.gitignore +++ b/hkmc2/shared/src/test/mlscript-compile/.gitignore @@ -1,4 +1,5 @@ *.mjs +*.wat !/Predef.mjs !/Runtime.mjs !/RuntimeJS.mjs diff --git a/hkmc2/shared/src/test/mlscript-compile/wasm/ImportValue.mls b/hkmc2/shared/src/test/mlscript-compile/wasm/ImportValue.mls new file mode 100644 index 0000000000..6f753b8cc8 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-compile/wasm/ImportValue.mls @@ -0,0 +1,7 @@ +#config(target: CompilationTarget.Wasm) + +import "./SimpleTarget.mls" + +module ImportValue with ... + +val value = SimpleTarget.value + 1 diff --git a/hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.mjs b/hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.mjs new file mode 100644 index 0000000000..ade66a08e7 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.mjs @@ -0,0 +1,41 @@ +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))" +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))" + +function binaryenCompileToModule(wat, importObject) { + const mod = binaryen.parseText(wat) + mod.setFeatures(binaryen.Features.All) + if (!mod.validate()) throw new Error("Generated WAT is invalid") + const modBuf = mod.emitBinary() + mod.dispose() + return WebAssembly.instantiate(modBuf, importObject) +} + +async function __mlx_buildSystem() { + const mem = new WebAssembly.Memory({ initial: 0 }) + const decodeUtf16 = new TextDecoder("utf-16le") + const intrinsicModule = await binaryenCompileToModule(__mlx_intrinsics_wat, {}) + return { + mem, + mlx_str_from_utf16: (ptr, byteLen) => + decodeUtf16.decode(new Uint8Array(mem.buffer, ptr, byteLen)), + ...intrinsicModule.instance.exports + } +} + +async function __mlx_importObject() { + const importObject = { + system: await __mlx_buildSystem() + } + + return importObject +} + +const __mlx_wasmPromise = __mlx_importObject() + .then(importObject => binaryenCompileToModule(__mlx_wat, importObject)) + +export const __mlx_wasm = () => __mlx_wasmPromise + +export default __mlx_wasm().then(({ instance }) => instance.exports["SimpleTarget"]) diff --git a/hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.mls b/hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.mls index 76c3b1aa51..3e4d39fa0d 100644 --- a/hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.mls +++ b/hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.mls @@ -1,3 +1,5 @@ #config(target: CompilationTarget.Wasm) -40 + 2 +module SimpleTarget with ... + +val value = 40 + 2 diff --git a/hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.wat b/hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.wat index a5b6a44b71..39f7fd3d85 100644 --- a/hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.wat +++ b/hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.wat @@ -1,12 +1,69 @@ (module (type $Object (sub (struct (field $$tag (mut i32))))) - (type $plus_impl (func (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any)))) + (type $SimpleTarget (sub $Object (struct (field $$tag (mut i32))))) + (type $SimpleTarget_init (func (param $this (ref null any)) (result (ref null any)))) + (type $SimpleTarget_ctor (func (result (ref null any)))) + (type $Unit (sub $Object (struct (field $$tag (mut i32))))) + (type $Unit_init (func (param $this (ref null any)) (result (ref null any)))) + (type $Unit_ctor (func (result (ref null any)))) (type $entry1 (func (result (ref null any)))) - (import "system" "plus_impl" (func $plus_impl (type $plus_impl))) + (type $start (func)) + (global $Unit$inst (export "Unit$inst") (mut (ref null $Unit)) (ref.null $Unit)) + (func $SimpleTarget_init (type $SimpleTarget_init) (param $this (ref null any)) (result (ref null any)) + (block (result (ref null any)) + (nop) + (nop) + (return + (local.get $this)))) + (func $SimpleTarget_ctor (export "SimpleTarget") (type $SimpleTarget_ctor) (result (ref null any)) + (local $this (ref null any)) + (block (result (ref null any)) + (local.set $this + (struct.new_default $SimpleTarget)) + (struct.set $SimpleTarget $$tag + (ref.cast (ref $SimpleTarget) + (local.get $this)) + (i32.const 1)) + (drop + (call $SimpleTarget_init + (local.get $this))) + (return + (local.get $this)))) + (func $Unit_init1 (type $Unit_init) (param $this (ref null any)) (result (ref null any)) + (block (result (ref null any)) + (nop) + (nop) + (return + (local.get $this)))) + (func $Unit_ctor1 (type $Unit_ctor) (result (ref null any)) + (local $this (ref null any)) + (block (result (ref null any)) + (local.set $this + (struct.new_default $Unit)) + (struct.set $Unit $$tag + (ref.cast (ref $Unit) + (local.get $this)) + (i32.const 2)) + (drop + (call $Unit_init1 + (local.get $this))) + (return + (local.get $this)))) + (func $start1 (type $start) + (block + (global.set $Unit$inst + (ref.cast (ref null $Unit) + (call $Unit_ctor1))))) (func $entry (export "entry") (type $entry1) (result (ref null any)) - (call $plus_impl - (ref.i31 - (i32.const 40)) - (ref.i31 - (i32.const 2)))) - (elem $entry declare func $entry)) \ No newline at end of file + (block (result (ref null any)) + (block + (nop) + (nop)) + (global.get $Unit$inst))) + (elem $SimpleTarget_init declare func $SimpleTarget_init) + (elem $SimpleTarget_ctor declare func $SimpleTarget_ctor) + (elem $Unit_init1 declare func $Unit_init1) + (elem $Unit_ctor1 declare func $Unit_ctor1) + (elem $start1 declare func $start1) + (elem $entry declare func $entry) + (start $start1)) \ No newline at end of file diff --git a/hkmc2/shared/src/test/mlscript/codegen/ImportMLsJS.mls b/hkmc2/shared/src/test/mlscript/codegen/ImportMLsJS.mls index f8328e5316..289e60553a 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/ImportMLsJS.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/ImportMLsJS.mls @@ -53,8 +53,7 @@ new! Option.Some(1) //│ = Some(1) -import "../../mlscript-compile/wasm/SimpleTarget.mjs" -//│ SimpleTarget = 42 +import "../../mlscript-compile/wasm/SimpleTarget.mls" -SimpleTarget + 1 -//│ = 43 +SimpleTarget.value + 1 +//│ = NaN From 78f4b733fbd8a81e5cfe74cae5358857a9205e32 Mon Sep 17 00:00:00 2001 From: FetBoba Date: Thu, 23 Apr 2026 07:58:47 +0800 Subject: [PATCH 08/12] Reverted unnecessary changes and moved Wasm related classes to Ctx.scala --- .../src/main/scala/hkmc2/MLsCompiler.scala | 16 +++------- .../scala/hkmc2/codegen/wasm/text/Ctx.scala | 32 +++++++++++++++++-- .../hkmc2/codegen/wasm/text/WatBuilder.scala | 13 ++------ .../src/test/mlscript/wasm/ClassMethods.mls | 2 +- 4 files changed, 38 insertions(+), 25 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala index 1bc8b2105b..9567888ce4 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala @@ -11,6 +11,8 @@ import hkmc2.semantics.* import hkmc2.syntax.Keyword.`override` import semantics.Elaborator.{Ctx, State} +import codegen.wasm.text.{WasmCompilation, WasmDependency} + class ParserSetup(file: io.Path, dbgParsing: Bool)(using state: Elaborator.State, raise: Raise, cctx: CompilerCtx): @@ -54,6 +56,8 @@ class MLsCompiler (using cctx: CompilerCtx, config: Config): import paths.* + + /** Module metadata collected during elaboration. */ private case class ModuleInfo( file: io.Path, @@ -63,18 +67,6 @@ class MLsCompiler hasQuote: Bool, ) - /** A Wasm module imported as a dependency. */ - private case class WasmDependency( - importPath: Str, - compiled: codegen.wasm.text.CompiledWasmModule, - ) - - /** Result of compiling a module to Wasm with its dependencies. */ - private case class WasmCompilation( - compiled: codegen.wasm.text.CompiledWasmModule, - dependencies: Seq[WasmDependency], - ) - // TODO adapt logic given DebugPrinter = new DebugPrinter val etl = new TraceLogger{override def doTrace: Bool = false} diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/Ctx.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/Ctx.scala index 49a7b5c159..79ab62fe05 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/Ctx.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/Ctx.scala @@ -138,10 +138,38 @@ final case class CompiledWasmModule( sessionExports: Seq[SessionBinding], ) +/** A Wasm module imported as a dependency. + * + * @param importPath + * the import statement's path as written in the source + * @param compiled + * the successfully compiled Wasm module + */ +final case class WasmDependency( + importPath: Str, + compiled: CompiledWasmModule, +) + +/** Result of compiling a module to Wasm with its dependencies. + * + * @param compiled + * the successfully compiled Wasm module + * @param dependencies + * all imported `.mls` modules compiled to Wasm + */ +final case class WasmCompilation( + compiled: CompiledWasmModule, + dependencies: Seq[WasmDependency], +) + /** Context for collecting session exports from a Wasm module. * * @param moduleName * The module name used for identifying exports in dependent modules. + * @param symbolsToExport + * The symbols from the current module that should be recorded as session exports. + * @param collectedBindings + * The session bindings accumulated while compiling the current module. */ final class SessionExportCtx( val moduleName: Str, @@ -459,7 +487,7 @@ class FunctionCtx(_params: Ls[ParamList], thisSym: Opt[InnerSymbol])(using Raise continueLabel = labels(label).continueLabel.map(cl => labels.last._2.scp.lookup_!(cl, N)), ) end FunctionCtx - + /** Generates a function body, providing an instance of [[FunctionCtx]] for parameter and locals tracking. * * Returns the result of the `mkBody` function along with the [[FunctionCtx]]. @@ -863,7 +891,7 @@ class Ctx extends ToWat: /** Checks whether the global variable scope contains the variable `sym`. */ def containsGlobal(sym: Symbol): Bool = namedGlobals.contains(sym) - + /** Returns all globals in this context. */ def getGlobals: Seq[Symbol] = namedGlobals.keys.toSeq diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala index 51b89645d6..8834082457 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala @@ -903,17 +903,10 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: case ty: StructType => ty.fieldsBySym case _ => lastWords(s"Cannot select field from non-struct type: ${structInfo.compType.toWat.mkString()}") - // Try direct lookup first val fieldIdx = symToField.get(sym).map(_.id).getOrElse: - // If direct lookup fails, try matching by qualified name - val symQualName = sym.toString - symToField.collectFirst: - case (fieldSym, field) if fieldSym.toString == symQualName => field.id - .getOrElse: - val availableFields = symToField.keys.iterator.map(_.toString).toSeq.sorted.mkString(", ") - lastWords( - s"Missing field `${sym.toString}` in struct `${thisSym.toString}` with type `${structInfo.toWat.mkString()}`. Available fields: $availableFields", - ) + lastWords( + s"Missing field `${sym.toString}` in struct `${thisSym.toString}` with type `${structInfo.toWat.mkString()}`", + ) FieldIdx(SymIdx(fieldIdx)) end fieldSelect diff --git a/hkmc2/shared/src/test/mlscript/wasm/ClassMethods.mls b/hkmc2/shared/src/test/mlscript/wasm/ClassMethods.mls index a286cee0a7..ffdd83c60d 100644 --- a/hkmc2/shared/src/test/mlscript/wasm/ClassMethods.mls +++ b/hkmc2/shared/src/test/mlscript/wasm/ClassMethods.mls @@ -170,7 +170,7 @@ c.Counter#inc().Counter#double().Counter#get() :todo let d = Counter(c.Counter#get()) -//│ /!!!\ Uncaught error: java.lang.Exception: Internal Error: Missing field `term:Counter/get` in struct `member:Counter` with type `(type $Counter (sub $Object (struct (field $$tag (mut i32)) (field $x (mut (ref null any))))))`. Available fields: term:$tag, term:Counter/x +//│ /!!!\ Uncaught error: java.lang.Exception: Internal Error: Missing field `term:Counter/get` in struct `member:Counter` with type `(type $Counter (sub $Object (struct (field $$tag (mut i32)) (field $x (mut (ref null any))))))` :todo d.Counter#inc().Counter#double().Counter#get() From 3b40dac422fbc29533efea5a4762a9e9d0abee22 Mon Sep 17 00:00:00 2001 From: FetBoba Date: Sat, 2 May 2026 22:13:12 +0800 Subject: [PATCH 09/12] Refactored MLsCompiler and added export extraction --- .../src/test/scala/hkmc2/CompilerTest.scala | 4 +- .../src/main/scala/hkmc2/CompilerCtx.scala | 31 +- .../src/main/scala/hkmc2/MLsCompiler.scala | 490 +++++++++--------- .../scala/hkmc2/codegen/wasm/text/Ctx.scala | 58 +-- .../hkmc2/codegen/wasm/text/WatBuilder.scala | 367 ++++++++----- .../main/scala/hkmc2/semantics/Importer.scala | 2 +- .../mlscript-compile/wasm/SimpleTarget.mjs | 34 +- .../mlscript-compile/wasm/SimpleTarget.wat | 12 +- .../test/mlscript/wasm/ClassInheritance.mls | 12 +- .../src/test/mlscript/wasm/ControlFlow.mls | 12 +- .../src/test/mlscript/wasm/ReplImports.mls | 19 + .../src/test/mlscript/wasm/SingletonUnit.mls | 12 +- 12 files changed, 580 insertions(+), 473 deletions(-) diff --git a/hkmc2/js/src/test/scala/hkmc2/CompilerTest.scala b/hkmc2/js/src/test/scala/hkmc2/CompilerTest.scala index 15b942bcfb..44a5cb18bd 100644 --- a/hkmc2/js/src/test/scala/hkmc2/CompilerTest.scala +++ b/hkmc2/js/src/test/scala/hkmc2/CompilerTest.scala @@ -110,9 +110,11 @@ class CompilerTest extends AnyFunSuite: assert(fs.exists(Path("/simpleWasm.mjs")), "Glue JavaScript file should be generated") assert(fs.exists(Path("/simpleWasm.wat")), "WAT file should be generated") + assert(fs.exists(Path("/RuntimeWASM.mjs")), "Shared Wasm runtime helper should be generated") + assert(fs.exists(Path("/RuntimeWASM.wat")), "Shared Wasm intrinsic WAT should be generated") val glue = fs.read("/simpleWasm.mjs") - assert(glue.contains("binaryenCompileToModule"), "Glue code should instantiate the WAT module") + assert(glue.contains("__mlx_compileWatFromUrl"), "Glue code should load module WAT from file") assert(glue.contains("export const __mlx_wasm"), "Glue code should expose the internal wasm loader") assert(glue.contains("export default"), "Glue code should export the module result by default") diff --git a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala index 968a6efd70..e12707516a 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala @@ -8,7 +8,7 @@ import mlscript.utils.*, shorthands.* import hkmc2.utils.* import hkmc2.Message.MessageContext import hkmc2.io -import utils.TraceLogger +import utils.TL import semantics.* import Elaborator.* @@ -34,7 +34,7 @@ class CompilerCtx( CompilerCtx(S(newFile, this), beingCompiled + newFile, fs, cache) def getElaboratedBlock - (file: io.Path, prelude: Ctx) + (file: io.Path, prelude: Ctx, full: Bool) (using TL, Raise, Config) : Artifact = @@ -62,18 +62,28 @@ class CompilerCtx( given CompilerCtx = this ParserSetup(file, dbgParsing = false) val resBlk = parse.resultBlk - given Elaborator.Ctx = prelude.copy(mode = Mode.Light).nestLocal("prelude") + val mode = if full then Mode.Full else Mode.Light + given Elaborator.Ctx = prelude.copy(mode = mode).nestLocal("prelude") val elab = given CompilerCtx = derive(parse.origin.fileName) Elaborator(tl, file.up, prelude) - val elabbed = elab.importFrom(resBlk) - Artifact(resBlk, elabbed._1, lastMod) + val (blk, _) = elab.importFrom(resBlk) + // Resolve once here so cached terms are not mutated again by later passes + // (diamond imports would otherwise re-run Resolver and trip expand()). + Resolver(summon[TL])(using summon[Raise], summon[State], summon[Ctx], summon[Config]) + .traverseBlock(blk)(using Resolver.ICtx.empty) + Artifact(resBlk, blk, lastMod, full) cache.upsert(file): case N => mk case cur @ S(art) => - if art.lastChangedTimestamp < lastMod then mk + if art.lastChangedTimestamp < lastMod || !art.mode && full then mk else art + + def getCachedElaboratedBlock(file: io.Path, full: Bool): Opt[Artifact] = + val lastMod = fs.getLastChangedTimestamp(file) + cache.elabCache.get(file).filter: art => + art.lastChangedTimestamp >= lastMod && (art.mode || !full) object CompilerCtx: @@ -88,7 +98,12 @@ end CompilerCtx object CompilerCache: - class Artifact(val tree: syntax.Tree.Block, val term: semantics.Term.Blk, val lastChangedTimestamp: Long) + class Artifact( + val tree: syntax.Tree.Block, + val term: semantics.Term.Blk, + val lastChangedTimestamp: Long, + val mode: Bool, + ) end CompilerCache @@ -110,5 +125,3 @@ trait CompilerCache: end CompilerCache - - diff --git a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala index 247701db69..92d41d7bd3 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala @@ -11,7 +11,9 @@ import hkmc2.semantics.* import hkmc2.syntax.Keyword.`override` import semantics.Elaborator.{Ctx, State} -import codegen.wasm.text.{WasmCompilation, WasmDependency} +import codegen.wasm.text.{ + CompiledWasmModule, SessionFunc, SessionGlobal, SessionSingleton, WatBuilder, +} class ParserSetup(file: io.Path, dbgParsing: Bool)(using state: Elaborator.State, raise: Raise, cctx: CompilerCtx): @@ -44,29 +46,18 @@ object MLsCompiler: def termFile: io.Path /** - * The compiler that compiles MLscript code into JavaScript modules. + * The compiler that compiles MLscript code into JavaScript or WebAssembly modules. * * @param paths required paths needed by the compiler * @param mkRaise generates a separate `Raise` function for each file. * @param config the compiler's configuration object - * @param fs the file system interface + * @param cctx the compilation context (including the file system interface) */ class MLsCompiler (paths: MLsCompiler.Paths, mkRaise: io.Path => Raise) (using cctx: CompilerCtx, config: Config): import paths.* - - - /** Module metadata collected during elaboration. */ - private case class ModuleInfo( - file: io.Path, - block: Term.Blk, - exportedSymbol: Opt[BlockMemberSymbol], - effectiveCfg: Config, - hasQuote: Bool, - ) - // TODO adapt logic given DebugPrinter = new DebugPrinter val etl = new TraceLogger{override def doTrace: Bool = false} @@ -78,70 +69,35 @@ class MLsCompiler var dbgParsing = false var dbgElab = false - /** Collects metadata about an elaborated module. */ - private def moduleInfo( - file: io.Path, - parsed: syntax.Tree.Block, - block: Term.Blk, - )(using Raise, Elaborator.State, Elaborator.Ctx): ModuleInfo = - def findQuote(t: semantics.Statement): Bool = t match - case Term.Quoted(_) | Term.Unquoted(_) => true - case Term.Ref(sym) => sym === Elaborator.State.termSymbol - case _ => t.subTerms.exists(findQuote) - - val resolver = Resolver(rtl) - resolver.traverseBlock(block)(using Resolver.ICtx.empty) - ModuleInfo( - file = file, - block = block, - exportedSymbol = parsed.definedSymbols.find(_._1 === file.baseName).map(_._2), - effectiveCfg = block.stats.collect: - case sc: SetConfig => sc.modify - .foldLeft(config): (cfg, modify) => - modify(cfg), - hasQuote = findQuote(block), - ) + /** Symbols a module wants preserved through optimization passes and surfaced to + * downstream importers: the exported module itself, its members, and their type symbols. */ + private def preservedSymbolsFor(exportedSymbol: Opt[BlockMemberSymbol]): Set[codegen.Local] = + val members = exportedSymbol.iterator.flatMap: sym => + sym.modOrObjTree.iterator.flatMap(_.definedSymbols.valuesIterator) + .toSet + (exportedSymbol.iterator ++ members.iterator ++ members.iterator.flatMap(_.tsym.toSeq)) + .map(sym => sym: codegen.Local).toSet - /** Elaborates and collects metadata for an imported module. */ - private def elaborateImportedModule( + /** JS-specific emit: build module source and write the `.mjs` file. */ + private def emitJs( file: io.Path, - prelude: Elaborator.Ctx, - )(using Elaborator.State, CompilerCtx): ModuleInfo = - val parentCctx = summon[CompilerCtx] - given Raise = mkRaise(file) - val artifact = etl.givenIn: - parentCctx.getElaboratedBlock(file, prelude) - prelude.nestLocal("file:" + file.baseName).givenIn: - given CompilerCtx = parentCctx.derive(file) - moduleInfo(file, artifact.tree, artifact.term) - - /** Lowers a module to intermediate representation with optimization passes. */ - private def lowerModule(module: ModuleInfo)(using Raise, Elaborator.State, Elaborator.Ctx): codegen.Program = - val blk = - module.effectiveCfg.target match - case CompilationTarget.JS => - new Term.Blk( - Import(State.runtimeSymbol, runtimeFile.toString, runtimeFile) :: - // Only import `Term.mls` when necessary. - (if module.hasQuote then - Import(State.termSymbol, termFile.toString, termFile) :: module.block.stats - else - module.block.stats), - module.block.res - ) - case CompilationTarget.Wasm => - module.block - module.effectiveCfg.givenIn: - val low = ltl.givenIn: - new codegen.Lowering() - with codegen.LoweringSelSanityChecks - val lowered = low.program(blk) - val simplified = ltl.givenIn: - codegen.BlockSimplifier(module.exportedSymbol.toSet)(lowered) - ltl.givenIn: - codegen.DeadParamElim(simplified) + wd: io.Path, + program: codegen.Program, + exportedSymbol: Opt[BlockMemberSymbol], + )(using Raise, State, Ctx, Config): Unit = + val jsb = ltl.givenIn: + codegen.js.JSBuilder() + val baseScp: utils.Scope = + utils.Scope.empty(utils.Scope.Cfg.default) + // * This line serves for `import.meta.url`, which retrieves directory and file names of mjs files. + // * Having `module id"import" with ...` in `prelude.mls` will generate `globalThis.import` that is undefined. + baseScp.addToBindings(Elaborator.State.importSymbol, "import", shadow = false) + val nestedScp = baseScp.nest + val je = nestedScp.givenIn: + jsb.program(program, exportedSymbol, wd) + cctx.fs.write(file.up / io.RelPath(s"${file.baseName}.mjs"), je.stripBreaks.mkString(100)) - /** Resolves an import path to the source `.mls` file if possible. */ + /** Resolves a Wasm module's `.mjs` import path back to its `.mls` source, if present on disk. */ private def resolveImportSource(importPath: Str, moduleDir: io.Path): Opt[io.Path] = val importedPath = if importPath.startsWith("/") @@ -152,200 +108,226 @@ class MLsCompiler resolved.optionIf(cctx.fs.exists(resolved)) else N - /** Compiles a module to JavaScript and writes the `.mjs` file. */ - private def emitJs( - module: ModuleInfo, - wd: io.Path, - program: codegen.Program, - )(using Raise, Elaborator.State, Elaborator.Ctx): Unit = - module.effectiveCfg.givenIn: - val jsb = ltl.givenIn: - codegen.js.JSBuilder() - val baseScp: utils.Scope = - utils.Scope.empty(utils.Scope.Cfg.default) - // * This line serves for `import.meta.url`, which retrieves directory and file names of mjs files. - // * Having `module id"import" with ...` in `prelude.mls` will generate `globalThis.import` that is undefined. - baseScp.addToBindings(Elaborator.State.importSymbol, "import", shadow = false) - val nestedScp = baseScp.nest - val je = nestedScp.givenIn: - jsb.program(program, module.exportedSymbol, wd) - cctx.fs.write(module.file.up / io.RelPath(s"${module.file.baseName}.mjs"), je.stripBreaks.mkString(100)) - - /** Generates JavaScript glue code for instantiating and importing a Wasm module. */ + /** JavaScript glue that loads the module's `.wat`, links its Wasm dependencies, + * and re-exports the program result. */ private def wasmGlue( wd: io.Path, - wat: Str, - intrinsicSupportWat: Str, - compiled: codegen.wasm.text.CompiledWasmModule, + moduleBaseName: Str, + compiled: CompiledWasmModule, exportedSymbol: Opt[BlockMemberSymbol], - dependencies: Seq[WasmDependency], + dependencies: Seq[(Str, Str)], ): Str = def relativeImportPath(path: Str): Str = if path.startsWith("/") then "./" + io.Path(path).relativeTo(wd).map(_.toString).getOrElse(path) else path - val dependencyImports = dependencies.zipWithIndex.map: - case (dependency, idx) => - s"""import { __mlx_wasm as __mlx_dep_$idx } from "${relativeImportPath(dependency.importPath)}";""" + case ((importPath, _), idx) => + s"""import { __mlx_wasm as __mlx_dep_$idx } from "${relativeImportPath(importPath)}";""" val dependencyImportStmts = if dependencyImports.isEmpty then "" else dependencyImports.mkString("", "\n", "\n\n") val dependencyBindings = dependencies.zipWithIndex.map: - case (dependency, idx) => - s""" importObject[${dependency.importPath.escaped}] = (await __mlx_dep_$idx()).instance.exports""" + case ((_, moduleName), idx) => + s""" importObject[${moduleName.escaped}] = (await __mlx_dep_$idx()).instance.exports""" val dependencyBindingStmts = if dependencyBindings.isEmpty then "" else dependencyBindings.mkString("", "\n", "\n") - val exportedValueExpr = - exportedSymbol.flatMap: sym => - compiled.sessionExports.collectFirst: - case func: codegen.wasm.text.SessionFunc if func.sym === sym => - s"""instance.exports[${func.exportName.escaped}]""" - case global: codegen.wasm.text.SessionGlobal if global.sym === sym => - s"""instance.exports[${global.exportName.escaped}].value""" - case singleton: codegen.wasm.text.SessionSingleton if singleton.blockSym === sym => - s"""instance.exports[${singleton.exportName.escaped}].value""" - .getOrElse(s"""instance.exports[${compiled.entryName.escaped}]()""") - s"""|import binaryen from "binaryen" + val exportedValueExpr = exportedSymbol.flatMap: sym => + compiled.sessionExports.collectFirst: + case f: SessionFunc if f.sym === sym => + if sym.modOrObjTree.nonEmpty then s"""instance.exports[${f.exportName.escaped}]()""" + else s"""instance.exports[${f.exportName.escaped}]""" + case g: SessionGlobal if g.sym === sym => s"""instance.exports[${g.exportName.escaped}].value""" + case s: SessionSingleton if s.blockSym === sym => s"""instance.exports[${s.exportName.escaped}].value""" + .getOrElse(s"""instance.exports[${compiled.entryName.escaped}]()""") + s"""|import { __mlx_compileWatFromUrl, __mlx_buildSystem } from "./RuntimeWASM.mjs" |$dependencyImportStmts | - |const __mlx_wat = ${wat.escaped} - |const __mlx_intrinsics_wat = ${intrinsicSupportWat.escaped} - | - |function binaryenCompileToModule(wat, importObject) { - | const mod = binaryen.parseText(wat) - | mod.setFeatures(binaryen.Features.All) - | if (!mod.validate()) throw new Error("Generated WAT is invalid") - | const modBuf = mod.emitBinary() - | mod.dispose() - | return WebAssembly.instantiate(modBuf, importObject) - |} - | - |async function __mlx_buildSystem() { - | const mem = new WebAssembly.Memory({ initial: ${compiled.systemMemMinPages} }) - | const decodeUtf16 = new TextDecoder("utf-16le") - | const intrinsicModule = await binaryenCompileToModule(__mlx_intrinsics_wat, {}) - | return { - | mem, - | mlx_str_from_utf16: (ptr, byteLen) => - | decodeUtf16.decode(new Uint8Array(mem.buffer, ptr, byteLen)), - | ...intrinsicModule.instance.exports - | } - |} + |const __mlx_wat_url = new URL("./${moduleBaseName}.wat", import.meta.url) | |async function __mlx_importObject() { | const importObject = { - | system: await __mlx_buildSystem() + | system: await __mlx_buildSystem(${compiled.systemMemMinPages}) | } |$dependencyBindingStmts | return importObject |} | |const __mlx_wasmPromise = __mlx_importObject() - | .then(importObject => binaryenCompileToModule(__mlx_wat, importObject)) + | .then(importObject => __mlx_compileWatFromUrl(__mlx_wat_url, importObject)) | |export const __mlx_wasm = () => __mlx_wasmPromise | - |export default __mlx_wasm().then(({ instance }) => $exportedValueExpr) + |const { instance } = await __mlx_wasm() + | + |export default $exportedValueExpr |""".stripMargin - /** Generates WAT for the intrinsics support module. */ - private def wasmIntrinsicSupportWat()(using Raise, Elaborator.State): Str = - val baseScp: utils.Scope = - utils.Scope.empty(utils.Scope.Cfg.default) - val watb = ltl.givenIn: - new codegen.wasm.text.WatBuilder() - baseScp.nest.givenIn: - watb.intrinsicSupportModule().mkString(100) - - /** Compiles a module to Wasm and writes `.wat` and `.mjs` files. */ - private def emitWasm( - module: ModuleInfo, - prelude: Elaborator.Ctx, - memo: mutable.Map[io.Path, WasmCompilation], - )(using Raise, Elaborator.State, CompilerCtx): codegen.wasm.text.CompiledWasmModule = - val moduleDir = module.file.up - val moduleBaseName = module.file.baseName - val moduleMjsFile = moduleDir / io.RelPath(s"$moduleBaseName.mjs") - val compilation = memo.getOrElseUpdate( - module.file, - locally: - given Raise = mkRaise(module.file) - prelude.givenIn: - val program = lowerModule(module) - val dependencies = mutable.ArrayBuffer.empty[WasmDependency] - program.imports.foreach: - case (sym, importPath) => - resolveImportSource(importPath, moduleDir) match - case S(sourceFile) => - val importedModule = elaborateImportedModule(sourceFile, prelude) - if importedModule.effectiveCfg.target =/= CompilationTarget.Wasm then - val importedTarget = importedModule.effectiveCfg.target.toString - raise( - ErrorReport( - msg"Wasm modules can only import Wasm-targeted `.mls` files; " + - msg"`${sourceFile.toString}` targets `$importedTarget`" -> - sym.toLoc :: Nil, - source = Diagnostic.Source.Compilation, - ), - ) - throw new IllegalStateException(s"Expected Wasm-targeted imported module at ${sourceFile.toString}") - dependencies += WasmDependency( - importPath, - emitWasm(importedModule, prelude, memo), - ) - case N => - raise( - ErrorReport( - msg"Wasm modules currently only support imports of Wasm-targeted `.mls` files; " + - msg"`${importPath}` cannot be resolved that way" -> - sym.toLoc :: Nil, - source = Diagnostic.Source.Compilation, - ), - ) + /** Source of the shared `RuntimeWASM.mjs` helper module. */ + private val wasmRuntimeMjsSource: Str = + """|import binaryen from "binaryen" + | + |async function __mlx_loadWatText(url) { + | if (url.protocol === "file:") { + | const { readFile } = await import("node:fs/promises") + | return readFile(url, "utf8") + | } + | const response = await fetch(url) + | if (!response.ok) throw new Error(`Failed to load WAT from ${url}`) + | return response.text() + |} + | + |export async function __mlx_compileWatFromUrl(url, importObject) { + | const wat = await __mlx_loadWatText(url) + | const mod = binaryen.parseText(wat) + | mod.setFeatures(binaryen.Features.All) + | if (!mod.validate()) throw new Error(`Generated WAT is invalid: ${url}`) + | const modBuf = mod.emitBinary() + | mod.dispose() + | return WebAssembly.instantiate(modBuf, importObject) + |} + | + |const __mlx_intrinsicsPromise = + | __mlx_compileWatFromUrl(new URL("./RuntimeWASM.wat", import.meta.url), {}) + | + |export async function __mlx_buildSystem(systemMemMinPages) { + | const mem = new WebAssembly.Memory({ initial: systemMemMinPages }) + | const decodeUtf16 = new TextDecoder("utf-16le") + | const intrinsicModule = await __mlx_intrinsicsPromise + | return { + | mem, + | mlx_str_from_utf16: (ptr, byteLen) => + | decodeUtf16.decode(new Uint8Array(mem.buffer, ptr, byteLen)), + | ...intrinsicModule.instance.exports + | } + |} + |""".stripMargin - val baseScp: utils.Scope = - utils.Scope.empty(utils.Scope.Cfg.default) - val nestedScp = baseScp.nest - val watb = ltl.givenIn: - new codegen.wasm.text.WatBuilder() - val sessionImports: Seq[codegen.wasm.text.SessionBinding] = - val seen = mutable.LinkedHashSet.empty[Str] - dependencies.iterator - .flatMap(_.compiled.sessionExports) - .filter: binding => - seen.add(binding.bindingKey) - .toSeq - WasmCompilation( - compiled = nestedScp.givenIn: - watb.program( - program, - module.exportedSymbol, - moduleDir, - sessionImports, - module.exportedSymbol.toSet, - moduleMjsFile.toString, - ), - dependencies = dependencies.toSeq, - ) - ) + /** Ensures the shared Wasm runtime helpers (`RuntimeWASM.{wat,mjs}`) are present + * in the given output directory. */ + private def ensureWasmRuntimeArtifacts(moduleDir: io.Path)(using Raise, State): Unit = + val watFile = moduleDir / io.RelPath("RuntimeWASM.wat") + val mjsFile = moduleDir / io.RelPath("RuntimeWASM.mjs") + if !cctx.fs.exists(watFile) then + val baseScp = utils.Scope.empty(utils.Scope.Cfg.default) + val watb = ltl.givenIn: + new WatBuilder() + val wat = baseScp.nest.givenIn: + watb.intrinsicSupportModule().mkString(100) + cctx.fs.write(watFile, wat) + if !cctx.fs.exists(mjsFile) then + cctx.fs.write(mjsFile, wasmRuntimeMjsSource) - val watStr = compilation.compiled.wat.mkString(100) - cctx.fs.write(moduleDir / io.RelPath(s"$moduleBaseName.wat"), watStr) - cctx.fs.write( - moduleMjsFile, - wasmGlue( - moduleDir, - watStr, - wasmIntrinsicSupportWat(), - compilation.compiled, - module.exportedSymbol, - compilation.dependencies, - ), - ) + /** Compiles an imported Wasm module, or scans cached full elaboration for its session exports. */ + private def compileWasmDep( + file: io.Path, + prelude: Ctx, + memo: mutable.Map[io.Path, Opt[CompiledWasmModule]], + )(using State, CompilerCtx): Opt[CompiledWasmModule] = + memo.get(file) match + case S(cached) => cached + case N => + given Raise = mkRaise(file) + val cachedArtifact = CompilerCtx.get.getCachedElaboratedBlock(file, full = true) + val artifact = cachedArtifact.getOrElse: + etl.givenIn: + CompilerCtx.get.getElaboratedBlock(file, prelude, true) + val blk = artifact.term + val effectiveCfg = Config.extractConfigFromStats(blk) + val compiled = + if effectiveCfg.target =/= CompilationTarget.Wasm then + raise(ErrorReport( + msg"Wasm modules can only import Wasm-targeted `.mls` files; " + + msg"`${file.toString}` targets `${effectiveCfg.target.toString}`" -> N :: Nil, + source = Diagnostic.Source.Compilation)) + N + else + prelude.givenIn: + val exportedSymbol = artifact.tree.definedSymbols.find(_._1 === file.baseName).map(_._2) + val preservedSymbols = preservedSymbolsFor(exportedSymbol) + effectiveCfg.givenIn: + val low = ltl.givenIn: + new codegen.Lowering() with codegen.LoweringSelSanityChecks + val le0 = low.program(blk) + val le1 = ltl.givenIn: + codegen.BlockSimplifier(preservedSymbols)(le0) + val program = ltl.givenIn: + codegen.DeadParamElim(le1) + val symbolsToExport = preservedSymbols ++ program.main.definedVars + cachedArtifact match + case S(_) => + val watb = ltl.givenIn: + new WatBuilder() + val exportScope = utils.Scope.empty(utils.Scope.Cfg.default).nest + val sessionExports = exportScope.givenIn: + watb.extractExports( + program.main, + Seq.empty, + symbolsToExport, + file.baseName, + ) + S(CompiledWasmModule(hkmc2.document.Document.empty, "entry", 0, sessionExports)) + case N => + emitWasm(file, program, exportedSymbol, preservedSymbols, prelude, memo) + memo(file) = compiled + compiled - compilation.compiled + /** Wasm-specific emit: compile any `.mls` dependencies, run `WatBuilder`, write + * `.wat`, `.mjs` glue, and ensure shared `RuntimeWASM.{wat,mjs}` helpers exist. */ + private def emitWasm( + file: io.Path, + program: codegen.Program, + exportedSymbol: Opt[BlockMemberSymbol], + preservedSymbols: Set[codegen.Local], + prelude: Ctx, + memo: mutable.Map[io.Path, Opt[CompiledWasmModule]], + )(using Raise, State, CompilerCtx): Opt[CompiledWasmModule] = + val moduleDir = file.up + val moduleBaseName = file.baseName + val edges = mutable.ArrayBuffer.empty[(Str, Str)] + val compiledDeps = mutable.ArrayBuffer.empty[CompiledWasmModule] + var failed = false + program.imports.foreach: + case (sym, importPath) => + resolveImportSource(importPath, moduleDir) match + case S(sourceFile) => + compileWasmDep(sourceFile, prelude, memo) match + case S(dep) => + edges += (importPath -> sourceFile.baseName) + compiledDeps += dep + case N => failed = true + case N => + raise(ErrorReport( + msg"Wasm modules currently only support imports of Wasm-targeted `.mls` files; " + + msg"`${importPath}` cannot be resolved that way" -> sym.toLoc :: Nil, + source = Diagnostic.Source.Compilation)) + failed = true + if failed then N + else + val sessionImports = + val seen = mutable.LinkedHashSet.empty[Str] + compiledDeps.iterator.flatMap(_.sessionExports).filter(b => seen.add(b.bindingKey)).toSeq + val baseScp = utils.Scope.empty(utils.Scope.Cfg.default) + val watb = ltl.givenIn: + new WatBuilder() + val symbolsToExport = preservedSymbols ++ program.main.definedVars + val compiled = baseScp.nest.givenIn: + watb.program( + program, + exportedSymbol, + moduleDir, + sessionImports, + symbolsToExport, + moduleBaseName, + ) + ensureWasmRuntimeArtifacts(moduleDir) + cctx.fs.write(moduleDir / io.RelPath(s"$moduleBaseName.wat"), compiled.wat.mkString(100)) + cctx.fs.write( + moduleDir / io.RelPath(s"$moduleBaseName.mjs"), + wasmGlue(moduleDir, moduleBaseName, compiled, exportedSymbol, edges.toSeq), + ) + S(compiled) def compileModule(file: io.Path): Unit = @@ -370,12 +352,42 @@ class MLsCompiler val elab = Elaborator(etl, wd, newCtx) val parsed = mainParse.resultBlk val (blk0, _) = elab.importFrom(parsed) - val module = moduleInfo(file, parsed, blk0) - module.effectiveCfg.target match + val resolver = Resolver(rtl) + resolver.traverseBlock(blk0)(using Resolver.ICtx.empty) + def findQuote(t: semantics.Statement): Bool = t match + case Term.Quoted(_) | Term.Unquoted(_) => true + case Term.Ref(sym) => sym === State.termSymbol + case _ => t.subTerms.exists(findQuote) + val hasQuote = findQuote(blk0) + val exportedSymbol = parsed.definedSymbols.find(_._1 === file.baseName).map(_._2) + val preservedSymbols = preservedSymbolsFor(exportedSymbol) + val effectiveCfg = Config.extractConfigFromStats(blk0) + val blk = effectiveCfg.target match case CompilationTarget.JS => - given Raise = mkRaise(file) - emitJs(module, wd, lowerModule(module)) + new Term.Blk( + Import(State.runtimeSymbol, runtimeFile.toString, runtimeFile) :: + // Only import `Term.mls` when necessary. + (if hasQuote then + Import(State.termSymbol, termFile.toString, termFile) :: blk0.stats + else + blk0.stats), + blk0.res + ) case CompilationTarget.Wasm => - emitWasm(module, newCtx, mutable.Map.empty) + blk0 + effectiveCfg.givenIn: + val low = ltl.givenIn: + new codegen.Lowering() + with codegen.LoweringSelSanityChecks + val le_0 = low.program(blk) + val le_1 = ltl.givenIn: + codegen.BlockSimplifier(preservedSymbols)(le_0) + val le_2 = ltl.givenIn: + codegen.DeadParamElim(le_1) + effectiveCfg.target match + case CompilationTarget.JS => + emitJs(file, wd, le_2, exportedSymbol) + case CompilationTarget.Wasm => + emitWasm(file, le_2, exportedSymbol, preservedSymbols, newCtx, mutable.Map.empty) end MLsCompiler diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/Ctx.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/Ctx.scala index 7bde676286..668cda70f9 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/Ctx.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/Ctx.scala @@ -49,9 +49,10 @@ final case class SessionFunc( moduleName: Str, exportName: Str, funcType: FunctionType, + aliasSyms: Seq[Local], ) extends SessionBinding: def bindingKey: Str = s"func:$moduleName:$exportName" - def bindingSyms: Seq[Local] = sym :: Nil + def bindingSyms: Seq[Local] = sym +: aliasSyms override def exportNameOpt: Opt[Str] = S(exportName) /** Metadata for an exported global that later Wasm REPL modules can import. @@ -137,61 +138,6 @@ final case class CompiledWasmModule( sessionExports: Seq[SessionBinding], ) -/** A Wasm module imported as a dependency. - * - * @param importPath - * the import statement's path as written in the source - * @param compiled - * the successfully compiled Wasm module - */ -final case class WasmDependency( - importPath: Str, - compiled: CompiledWasmModule, -) - -/** Result of compiling a module to Wasm with its dependencies. - * - * @param compiled - * the successfully compiled Wasm module - * @param dependencies - * all imported `.mls` modules compiled to Wasm - */ -final case class WasmCompilation( - compiled: CompiledWasmModule, - dependencies: Seq[WasmDependency], -) - -/** Context for collecting session exports from a Wasm module. - * - * @param moduleName - * The module name used for identifying exports in dependent modules. - * @param symbolsToExport - * The symbols from the current module that should be recorded as session exports. - * @param collectedBindings - * The session bindings accumulated while compiling the current module. - */ -final class SessionExportCtx( - val moduleName: Str, - val symbolsToExport: Set[Local], - val collectedBindings: ArrayBuf[SessionBinding], -): - def shouldExport(sym: Local): Bool = symbolsToExport(sym) - - def emit(binding: SessionBinding): Unit = - collectedBindings += binding - - def freshCollector(): SessionExportCtx = - SessionExportCtx(moduleName, symbolsToExport, ArrayBuf.empty) -end SessionExportCtx - -object SessionExportCtx: - def apply( - moduleName: Str, - symbolsToExport: Set[Local], - collectedBindings: ArrayBuf[SessionBinding], - ): SessionExportCtx = - new SessionExportCtx(moduleName, symbolsToExport, collectedBindings) - /** A Wasm function and its associated information. * * Each instance of [[FuncInfo]] represents a single function definition in a WebAssembly module. diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala index 2dbcb81645..dc225c9c43 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala @@ -65,6 +65,16 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: private def baseObjectRefType(nullable: Bool)(using Ctx): RefType = RefType(baseObjectTypeIdx, nullable = nullable) + private def registerBaseObjectType()(using Ctx): Unit = + ctx.addType( + sym = S(baseObjectSym), + TypeInfo( + id = SymIdx("Object"), + StructType(Seq(tagFieldSym -> Field(I32Type, mutable = true, id = "$tag"))), + objectTag = S(ctx.getFreshObjectTag() ensuring (_ == 0)), + ), + ) + /** True if this top-level class can be declared as a Wasm struct type. */ private def isSupportedTopLevelClass(defn: ClsLikeDefn): Bool = defn.owner.isEmpty @@ -102,40 +112,21 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: bufferable = N, )(N) - /** Registers the synthetic `Unit` singleton. */ - private def RegisterUnitSingleton()(using Ctx, FunctionCtx, Raise, Scope, SessionExportCtx): Unit = + private def ensureSyntheticUnitPredeclared()(using Ctx, Raise, Scope): Unit = val unitDefn = syntheticUnitDefn - val singletonOwner = unitDefn.isym match - case mos: ModuleOrObjectSymbol => S(mos) - case _ => N - if ctx.containsSingleton(unitDefn.sym) then return - if ctx.getType(unitDefn.sym).isEmpty then predeclareClassType(unitDefn) predeclareClassInit(unitDefn) predeclareClassConstructor(unitDefn) - returningTerm(Define(unitDefn, End(""))) + /** Registers the synthetic `Unit` singleton. */ + private def RegisterUnitSingleton()(using Ctx, FunctionCtx, Raise, Scope): Unit = + val unitDefn = syntheticUnitDefn + if ctx.containsSingleton(unitDefn.sym) then return - val typeInfo = ctx.getTypeInfo_!(unitDefn.sym) - val singletonInfo = ctx.getSingletonInfo(unitDefn.sym) getOrElse: - lastWords("Missing singleton metadata for synthetic Unit object") - // Record session metadata for the synthetic Unit singleton. - summon[SessionExportCtx].emit(SessionClass( - sym = unitDefn.sym, - typeInfo = typeInfo, - runtimeTags = ctx.getAllRuntimeTags(unitDefn.sym) getOrElse: - LinkedHashSet(ctx.getRuntimeClassTag_!(unitDefn.sym)) - , - aliasSyms = singletonOwner.toSeq, - )) - summon[SessionExportCtx].emit(SessionSingleton( - blockSym = unitDefn.sym, - objectSym = singletonOwner, - moduleName = summon[SessionExportCtx].moduleName, - exportName = singletonInfo.globalName, - globalTy = singletonInfo.globalTy, - )) + ensureSyntheticUnitPredeclared() + + returningTerm(Define(unitDefn, End(""))) end RegisterUnitSingleton /** Registers eager singleton runtime state by creating its global and start-init action. */ @@ -373,10 +364,12 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: p.sym -> SymIdx(p.sym.nme) val initId = defn.sym .optionIf: sym => - !(defn.k is syntax.Obj) && sym.nameIsMeaningful + (!(defn.k is syntax.Obj) || defn.sym === State.unitBlockMemberSymbol) && sym.nameIsMeaningful .map: sym => SymIdx(s"${sym.nme}_init") - predeclareClassFunc(defn, "init", initParams, S(initFuncSym(defn.sym)), initId, N) + val initExportName = + S(s"${State.unitBlockMemberSymbol.nme}_init").filter(_ => defn.sym === State.unitBlockMemberSymbol) + predeclareClassFunc(defn, "init", initParams, S(initFuncSym(defn.sym)), initId, initExportName) /** Declares one top-level class constructor. */ private def predeclareClassConstructor(defn: ClsLikeDefn)(using Ctx, Raise, Scope): Unit = @@ -385,13 +378,15 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: p.sym -> SymIdx(p.sym.nme) val ctorId = defn.sym .optionIf: sym => - !(defn.k is syntax.Obj) && sym.nameIsMeaningful + (!(defn.k is syntax.Obj) || defn.sym === State.unitBlockMemberSymbol) && sym.nameIsMeaningful .map: sym => SymIdx(s"${sym.nme}_ctor") val ctorExportName = defn.sym .optionIf: sym => - !(defn.k is syntax.Obj) && sym.nameIsMeaningful + (!(defn.k is syntax.Obj) || defn.sym === State.unitBlockMemberSymbol) && sym.nameIsMeaningful .map(_.nme) + .map: name => + if defn.sym === State.unitBlockMemberSymbol then s"${State.unitBlockMemberSymbol.nme}_ctor" else name predeclareClassFunc(defn, "ctor", ctorParams, S(defn.sym), ctorId, ctorExportName) /** Collects the symbols that should live in mutable globals so later REPL blocks can import them. @@ -401,7 +396,7 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: */ private def collectSessionGlobalSymbols( b: Block, - sessionExportCtx: SessionExportCtx, + symbolsToExport: Set[Local], ): Set[Symbol] = def restOf(block: Block): Opt[Block] = block match case Define(_, rst) => S(rst) @@ -418,11 +413,11 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: recur(body) case Begin(sub, rst) => recur(sub) ++ recur(rst) - case Define(ValDefn(_, sym, _), rst) if sessionExportCtx.shouldExport(sym) => + case Define(ValDefn(_, sym, _), rst) if symbolsToExport(sym) => recur(rst) + sym case Define(_, rst) => recur(rst) - case Assign(sym: Symbol, _, rst) if sessionExportCtx.shouldExport(sym) => + case Assign(sym: Symbol, _, rst) if symbolsToExport(sym) => recur(rst) + sym case _: BlockTail => Set.empty @@ -432,10 +427,189 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: recur(b) end collectSessionGlobalSymbols + def extractExports( + block: Block, + sessionImports: Seq[SessionBinding], + symbolsToExport: Set[Local], + currentModuleName: Str, + )(using Raise, Scope): Seq[SessionBinding] = + val ctx = Ctx.empty + given Ctx = ctx + val exports = ArrayBuf.empty[SessionBinding] + + registerBaseObjectType() + registerSessionImports(sessionImports) + + collectSessionGlobalSymbols(block, symbolsToExport).toSeq.sortBy(_.uid).foreach: sym => + exports += SessionGlobal( + sym = sym, + moduleName = currentModuleName, + exportName = sym.nme, + globalType = GlobalType(RefType.anyref, mutable = true), + ) + + def scanTopLevel(block: Block)(onExportedDefn: Defn => Unit): Unit = + block match + case Scoped(_, body) => + scanTopLevel(body)(onExportedDefn) + case Begin(sub, rst) => + scanTopLevel(sub)(onExportedDefn) + scanTopLevel(rst)(onExportedDefn) + case Define(defn, rst) => + if symbolsToExport(defn.sym) then onExportedDefn(defn) + scanTopLevel(rst)(onExportedDefn) + case Match(_, _, _, rst) => + scanTopLevel(rst)(onExportedDefn) + case TryBlock(_, _, rst) => + scanTopLevel(rst)(onExportedDefn) + case Label(_, _, _, rst) => + scanTopLevel(rst)(onExportedDefn) + case Assign(_, _, rst) => + scanTopLevel(rst)(onExportedDefn) + case AssignField(_, _, _, rst) => + scanTopLevel(rst)(onExportedDefn) + case AssignDynField(_, _, _, _, rst) => + scanTopLevel(rst)(onExportedDefn) + case _: BlockTail => + () + + var usesUnit = false + new BlockTraverser: + override def applyValue(v: Value): Unit = v match + case Value.Ref(l, disamb) => + if (l is State.unitSymbol) || disamb.contains(State.unitSymbol) then + usesUnit = true + super.applyValue(v) + case _ => + super.applyValue(v) + + override def applyPath(p: Path): Unit = p match + case sel @ Select(_, _) => + sel.symbol.foreach: + case sym: ModuleOrObjectSymbol if sym is State.unitSymbol => + usesUnit = true + case _ => () + super.applyPath(sel) + case _ => + super.applyPath(p) + .applyBlock(block) + + boundary[Unit]: + given Raise = diag => + diag match + case _: ErrorReport => break(()) + case _ => () + val ordered = sortTopLevelClasses(collectTopLevelClassDefns(block)) + ordered.foreach(predeclareClassType) + predeclareClassTags(ordered) + ordered.foreach(predeclareClassInit) + ordered.foreach(predeclareClassConstructor) + ordered.foreach(predeclareClassMethods) + + if usesUnit && !ctx.containsSingleton(State.unitBlockMemberSymbol) then + ensureSyntheticUnitPredeclared() + registerSingletonInit(syntheticUnitDefn, ctx.getType_!(State.unitBlockMemberSymbol)) + val unitDefn = syntheticUnitDefn + val singletonOwner = unitDefn.isym match + case mos: ModuleOrObjectSymbol => S(mos) + case _ => N + val typeInfo = ctx.getTypeInfo_!(unitDefn.sym) + val singletonInfo = ctx.getSingletonInfo(unitDefn.sym) getOrElse: + lastWords("Missing singleton metadata for synthetic Unit object") + val initSym = initFuncSym(unitDefn.sym) + exports += SessionClass( + sym = unitDefn.sym, + typeInfo = typeInfo, + runtimeTags = ctx.getAllRuntimeTags(unitDefn.sym) getOrElse: + LinkedHashSet(ctx.getRuntimeClassTag_!(unitDefn.sym)) + , + aliasSyms = singletonOwner.toSeq, + ) + ctx.getFuncInfo(initSym).foreach: initInfo => + exports += SessionFunc( + sym = initSym, + moduleName = currentModuleName, + exportName = initInfo.exportName.getOrElse(s"${State.unitBlockMemberSymbol.nme}_init"), + funcType = FunctionType(initInfo.getSignatureType), + aliasSyms = singletonOwner.toSeq, + ) + ctx.getFuncInfo(unitDefn.sym).foreach: ctorInfo => + exports += SessionFunc( + sym = unitDefn.sym, + moduleName = currentModuleName, + exportName = ctorInfo.exportName.getOrElse(s"${State.unitBlockMemberSymbol.nme}_ctor"), + funcType = FunctionType(ctorInfo.getSignatureType), + aliasSyms = singletonOwner.toSeq, + ) + exports += SessionSingleton( + blockSym = unitDefn.sym, + objectSym = singletonOwner, + moduleName = currentModuleName, + exportName = singletonInfo.globalName, + globalTy = singletonInfo.globalTy, + ) + + scanTopLevel(block): + case FunDefn(owner, sym, _, ps :: _, _) if owner.isEmpty && sym.nameIsMeaningful => + exports += SessionFunc( + sym = sym, + moduleName = currentModuleName, + exportName = sym.nme, + funcType = FunctionType( + params = ps.params.map(p => WasmParam(SymIdx(p.sym.nme), RefType.anyref)), + results = Seq(Result(RefType.anyref)), + ), + aliasSyms = Nil, + ) + case clsLikeDefn: ClsLikeDefn => + ctx.getTypeInfo(clsLikeDefn.sym).foreach: typeInfo => + val isSingletonObj = clsLikeDefn.k is syntax.Obj + val tagValue = typeInfo.objectTag getOrElse: + lastWords(s"Expected class ${clsLikeDefn.sym} to have an object tag") + exports += SessionClass( + sym = clsLikeDefn.sym, + typeInfo = typeInfo, + runtimeTags = ctx.getAllRuntimeTags(clsLikeDefn.sym).getOrElse(LinkedHashSet(tagValue)), + aliasSyms = clsLikeDefn.isym match + case mos: ModuleOrObjectSymbol => mos :: Nil + case _ => Nil, + ) + if !isSingletonObj && clsLikeDefn.sym.nameIsMeaningful then + val ctorParams = clsLikeDefn.paramsOpt.fold(Seq.empty[WasmParam]): ps => + ps.params.map(p => WasmParam(SymIdx(p.sym.nme), RefType.anyref)) + exports += SessionFunc( + sym = clsLikeDefn.sym, + moduleName = currentModuleName, + exportName = clsLikeDefn.sym.nme, + funcType = FunctionType( + params = ctorParams, + results = Seq(Result(RefType.anyref)), + ), + aliasSyms = Nil, + ) + if isSingletonObj then + registerSingletonInit(clsLikeDefn, ctx.getType_!(clsLikeDefn.sym)) + ctx.getSingletonInfo(clsLikeDefn.sym).foreach: info => + val singletonOwner = clsLikeDefn.isym match + case mos: ModuleOrObjectSymbol => S(mos) + case _ => N + exports += SessionSingleton( + blockSym = clsLikeDefn.sym, + objectSym = singletonOwner, + moduleName = currentModuleName, + exportName = info.globalName, + globalTy = info.globalTy, + ) + case _ => () + + val seen = LinkedHashSet.empty[Str] + exports.filter(binding => seen.add(binding.bindingKey)).toSeq + end extractExports + /** Declares a mutable exported global for a REPL-visible binding produced by the current block. */ private def registerSessionGlobal( sym: Symbol, - )(using Ctx, Raise, Scope, SessionExportCtx): Unit = + )(using Ctx, Raise, Scope): Unit = if ctx.containsGlobal(sym) then return val exportName = sym.nme ctx.addGlobal( @@ -447,18 +621,28 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: exportName = S(exportName), ), ) - summon[SessionExportCtx].emit(SessionGlobal( - sym = sym, - moduleName = summon[SessionExportCtx].moduleName, - exportName = exportName, - globalType = GlobalType(RefType.anyref, mutable = true), - )) end registerSessionGlobal /** Registers imported REPL bindings into the current module before codegen starts. */ private def registerSessionImports( sessionImports: Seq[SessionBinding], )(using Ctx, Raise, Scope): Unit = + val usedFuncTypeNames = LinkedHashSet("Object") + sessionImports.foreach: + case cls: SessionClass => usedFuncTypeNames += cls.typeInfo.id.id + case _ => () + + def sessionFuncTypeName(func: SessionFunc): SymIdx = + val base = func.exportName + val name = + if usedFuncTypeNames.add(base) then base + else + Iterator.from(1) + .map(i => s"${base}${i.toString}") + .find(usedFuncTypeNames.add) + .getOrElse(lastWords(s"Could not allocate session function type name for `$base`")) + SymIdx(name) + sessionImports.foreach: case cls: SessionClass => if ctx.getType(cls.sym).isEmpty then @@ -471,7 +655,7 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: val funcName = scope.allocateOrGetName(func.sym) val typeIdx = ctx.addType( sym = N, - TypeInfo(id = N, func.funcType), + TypeInfo(id = S(sessionFuncTypeName(func)), func.funcType), ) ctx.addFunctionImport( S(func.sym), @@ -636,7 +820,7 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: /** Compiles a class init body under its own Wasm-local frame with explicit `this`. */ private def setupInitLocals( clsLikeDefn: ClsLikeDefn, - )(using Ctx, Raise, Scope, SessionExportCtx): (Expr, FunctionCtx) = + )(using Ctx, Raise, Scope): (Expr, FunctionCtx) = genFuncBody(clsLikeDefn.paramsOpt.toList, thisSym = S(clsLikeDefn.isym)): val thisVar = funcCtx.lookupLocal_!(clsLikeDefn.isym, N) val preCtorWat = compilePreCtor(clsLikeDefn, thisVar) @@ -657,7 +841,7 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: private def compilePreCtor( clsLikeDefn: ClsLikeDefn, thisVar: LocalIdx, - )(using Ctx, FunctionCtx, Raise, Scope, SessionExportCtx): Expr = + )(using Ctx, FunctionCtx, Raise, Scope): Expr = def withRest(block: NonBlockTail, rest: Block): Block = block match case Scoped(syms, _) => Scoped(syms, rest) case Begin(sub, _) => Begin(sub, rest) @@ -719,7 +903,7 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: private def normalizeEntryExpr( expr: Expr, isAbortive: Bool, - )(using Ctx, FunctionCtx, Raise, Scope, SessionExportCtx): Expr = + )(using Ctx, FunctionCtx, Raise, Scope): Expr = if expr.resultTypes.isEmpty && !isAbortive then blockInstr( label = N, @@ -771,7 +955,7 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: loc: Opt[Loc], errCtx: Str, errExtra: => Str, - )(using Ctx, FunctionCtx, Raise, Scope, SessionExportCtx): Expr => Expr = + )(using Ctx, FunctionCtx, Raise, Scope): Expr => Expr = fld match case Value.Lit(IntLit(value)) if value.isValidInt => val idx = value.toInt @@ -876,7 +1060,7 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: ) end getVar - def argument(a: Arg)(using Ctx, FunctionCtx, Raise, Scope, SessionExportCtx): Expr = + def argument(a: Arg)(using Ctx, FunctionCtx, Raise, Scope): Expr = if a.spread.nonEmpty then errExpr( Ls(msg"WatBackend::argument for spread expression not implemented yet" -> a.value.toLoc), @@ -884,10 +1068,10 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: ) else result(a.value) - def operand(a: Arg)(using Ctx, FunctionCtx, Raise, Scope, SessionExportCtx): Expr = + def operand(a: Arg)(using Ctx, FunctionCtx, Raise, Scope): Expr = if a.spread.nonEmpty then die else subexpression(a.value) - def subexpression(r: codegen.Result)(using Ctx, FunctionCtx, Raise, Scope, SessionExportCtx): Expr = r match + def subexpression(r: codegen.Result)(using Ctx, FunctionCtx, Raise, Scope): Expr = r match case r: Lambda => errExpr( Ls(msg"WatBuilder::subexpression for Lambda not implemented yet" -> r.toLoc), @@ -918,7 +1102,7 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: sym.asBlkMember.filter: methodSym => methodSym.asTrm.exists(_.owner.exists(_.asCls.isDefined)) && ctx.getFunc(methodSym).nonEmpty - def result(r: codegen.Result)(using Ctx, FunctionCtx, Raise, Scope, SessionExportCtx): Expr = r match + def result(r: codegen.Result)(using Ctx, FunctionCtx, Raise, Scope): Expr = r match case Value.This(sym) => // TODO(Derppening): Add type tracking and refinement for locals, remove the `ref.cast` ref.cast( @@ -1333,7 +1517,7 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: case S(_) => drop(expr) case N => expr - def returningTerm(t: Block)(using Ctx, FunctionCtx, Raise, Scope, SessionExportCtx): Expr = + def returningTerm(t: Block)(using Ctx, FunctionCtx, Raise, Scope): Expr = t match case Assign(l, r, rst) if l is State.noSymbol => val rExpr = result(r) @@ -1524,13 +1708,6 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: body = bodyWat, ) ctx.addFunc(S(defn.sym), funcInfo) - if summon[SessionExportCtx].shouldExport(defn.sym) then - summon[SessionExportCtx].emit(SessionFunc( - sym = defn.sym, - moduleName = summon[SessionExportCtx].moduleName, - exportName = sym.nme, - funcType = FunctionType(funcInfo.getSignatureType), - )) nop else @@ -1679,42 +1856,8 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: lastWords( s"Class method `$methodDefn` with multiple parameter lists should be rejected in predeclaration pass", ) - if summon[SessionExportCtx].shouldExport(clsLikeDefn.sym) then - summon[SessionExportCtx].emit(SessionClass( - sym = clsLikeDefn.sym, - typeInfo = typeinfo, - runtimeTags = ctx.getAllRuntimeTags(clsLikeDefn.sym).getOrElse(LinkedHashSet(tagValue)), - aliasSyms = clsLikeDefn.isym match - case mos: ModuleOrObjectSymbol => mos :: Nil - case _ => Nil, - )) - if !isSingletonObj && clsLikeDefn.sym.nameIsMeaningful then - summon[SessionExportCtx].emit(SessionFunc( - sym = clsLikeDefn.sym, - moduleName = summon[SessionExportCtx].moduleName, - exportName = clsLikeDefn.sym.nme, - funcType = FunctionType( - SignatureType( - params = ctorFnCtx.params.map(p => WasmParam(p._2, RefType.anyref)), - results = Seq(Result(RefType.anyref)), - ), - ), - )) - end if if isSingletonObj then registerSingletonInit(clsLikeDefn, typeref) - if summon[SessionExportCtx].shouldExport(clsLikeDefn.sym) then - ctx.getSingletonInfo(clsLikeDefn.sym).foreach: info => - val singletonOwner = clsLikeDefn.isym match - case mos: ModuleOrObjectSymbol => S(mos) - case _ => N - summon[SessionExportCtx].emit(SessionSingleton( - blockSym = clsLikeDefn.sym, - objectSym = singletonOwner, - moduleName = summon[SessionExportCtx].moduleName, - exportName = info.globalName, - globalTy = info.globalTy, - )) nop @@ -2041,13 +2184,14 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: preservedSessionSymbols: Set[Local], currentModuleName: Str, )(using Raise, Scope): CompiledWasmModule = - val sessionExportCtx = SessionExportCtx( - moduleName = currentModuleName, - symbolsToExport = preservedSessionSymbols, - collectedBindings = ArrayBuf.empty, - ) - given SessionExportCtx = sessionExportCtx - + val exportScope = utils.Scope.empty(utils.Scope.Cfg.default).nest + val sessionExports = exportScope.givenIn: + extractExports( + p.main, + sessionImports, + preservedSessionSymbols, + currentModuleName, + ) val ctx = Ctx.empty given Ctx = ctx @@ -2058,25 +2202,16 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: ).fold(0)(_.memType.lim.min) def compiledModule(entryName: Str): CompiledWasmModule = - CompiledWasmModule(ctx.toWat, entryName, systemMemMinPages, sessionExportCtx.collectedBindings.toSeq) + CompiledWasmModule(ctx.toWat, entryName, systemMemMinPages, sessionExports) // Create base Object struct with tag field that all other structs will inherit - ctx.addType( - sym = S(baseObjectSym), - TypeInfo( - id = SymIdx("Object"), - StructType(Seq(tagFieldSym -> Field(I32Type, mutable = true, id = "$tag"))), - objectTag = S(ctx.getFreshObjectTag() ensuring (_ == 0)), - ), - ) + registerBaseObjectType() registerSessionImports(sessionImports) - collectSessionGlobalSymbols( - p.main, - sessionExportCtx, - ).toSeq.sortBy(_.uid).foreach: sym => - registerSessionGlobal(sym) + sessionExports.foreach: + case global: SessionGlobal => registerSessionGlobal(global.sym) + case _ => () boundary[CompiledWasmModule]: val outerRaise = summon[Raise] @@ -2187,20 +2322,20 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: def nonNestedScoped( blk: Block, - )(k: Block => Expr)(using Ctx, FunctionCtx, Raise, Scope, SessionExportCtx): Expr = blk match + )(k: Block => Expr)(using Ctx, FunctionCtx, Raise, Scope): Expr = blk match case Scoped(syms, body) => blockPreamble(syms.view.filter(body.freeVars)) k(body) case _ => k(blk) - def block(t: Block)(using Ctx, FunctionCtx, Raise, Scope, SessionExportCtx): Expr = + def block(t: Block)(using Ctx, FunctionCtx, Raise, Scope): Expr = nonNestedScoped(t)(returningTerm) def setupFunction( thisParam: Opt[InnerSymbol], params: ParamList, body: Block, - )(using Ctx, Raise, Scope, SessionExportCtx): (Expr, FunctionCtx) = + )(using Ctx, Raise, Scope): (Expr, FunctionCtx) = genFuncBody(params :: Nil, thisSym = thisParam): block(body) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala index 83915846a0..c920c6d6d6 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala @@ -54,7 +54,7 @@ class Importer: val sym = tl.trace(s">>> Importing $file"): given TL = tl - val artifact = cctx.getElaboratedBlock(file, prelude) + val artifact = cctx.getElaboratedBlock(file, prelude, cfg.target is CompilationTarget.Wasm) artifact.tree.definedSymbols.find(_._1 === nme) match case Some(nme -> imsym) => imsym case None => lastWords(s"File $file does not define a symbol named $nme") diff --git a/hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.mjs b/hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.mjs index ade66a08e7..7778558f04 100644 --- a/hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.mjs +++ b/hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.mjs @@ -1,41 +1,21 @@ -import binaryen from "binaryen" +import { __mlx_compileWatFromUrl, __mlx_buildSystem } from "./RuntimeWASM.mjs" -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))" - -function binaryenCompileToModule(wat, importObject) { - const mod = binaryen.parseText(wat) - mod.setFeatures(binaryen.Features.All) - if (!mod.validate()) throw new Error("Generated WAT is invalid") - const modBuf = mod.emitBinary() - mod.dispose() - return WebAssembly.instantiate(modBuf, importObject) -} - -async function __mlx_buildSystem() { - const mem = new WebAssembly.Memory({ initial: 0 }) - const decodeUtf16 = new TextDecoder("utf-16le") - const intrinsicModule = await binaryenCompileToModule(__mlx_intrinsics_wat, {}) - return { - mem, - mlx_str_from_utf16: (ptr, byteLen) => - decodeUtf16.decode(new Uint8Array(mem.buffer, ptr, byteLen)), - ...intrinsicModule.instance.exports - } -} +const __mlx_wat_url = new URL("./SimpleTarget.wat", import.meta.url) async function __mlx_importObject() { const importObject = { - system: await __mlx_buildSystem() + system: await __mlx_buildSystem(0) } return importObject } const __mlx_wasmPromise = __mlx_importObject() - .then(importObject => binaryenCompileToModule(__mlx_wat, importObject)) + .then(importObject => __mlx_compileWatFromUrl(__mlx_wat_url, importObject)) export const __mlx_wasm = () => __mlx_wasmPromise -export default __mlx_wasm().then(({ instance }) => instance.exports["SimpleTarget"]) +const { instance } = await __mlx_wasm() + +export default instance.exports["SimpleTarget"]() diff --git a/hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.wat b/hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.wat index 39f7fd3d85..4c850a69e0 100644 --- a/hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.wat +++ b/hkmc2/shared/src/test/mlscript-compile/wasm/SimpleTarget.wat @@ -29,13 +29,13 @@ (local.get $this))) (return (local.get $this)))) - (func $Unit_init1 (type $Unit_init) (param $this (ref null any)) (result (ref null any)) + (func $Unit_init (export "Unit_init") (type $Unit_init) (param $this (ref null any)) (result (ref null any)) (block (result (ref null any)) (nop) (nop) (return (local.get $this)))) - (func $Unit_ctor1 (type $Unit_ctor) (result (ref null any)) + (func $Unit_ctor (export "Unit_ctor") (type $Unit_ctor) (result (ref null any)) (local $this (ref null any)) (block (result (ref null any)) (local.set $this @@ -45,7 +45,7 @@ (local.get $this)) (i32.const 2)) (drop - (call $Unit_init1 + (call $Unit_init (local.get $this))) (return (local.get $this)))) @@ -53,7 +53,7 @@ (block (global.set $Unit$inst (ref.cast (ref null $Unit) - (call $Unit_ctor1))))) + (call $Unit_ctor))))) (func $entry (export "entry") (type $entry1) (result (ref null any)) (block (result (ref null any)) (block @@ -62,8 +62,8 @@ (global.get $Unit$inst))) (elem $SimpleTarget_init declare func $SimpleTarget_init) (elem $SimpleTarget_ctor declare func $SimpleTarget_ctor) - (elem $Unit_init1 declare func $Unit_init1) - (elem $Unit_ctor1 declare func $Unit_ctor1) + (elem $Unit_init declare func $Unit_init) + (elem $Unit_ctor declare func $Unit_ctor) (elem $start1 declare func $start1) (elem $entry declare func $entry) (start $start1)) \ No newline at end of file diff --git a/hkmc2/shared/src/test/mlscript/wasm/ClassInheritance.mls b/hkmc2/shared/src/test/mlscript/wasm/ClassInheritance.mls index 18475ed572..0b03067075 100644 --- a/hkmc2/shared/src/test/mlscript/wasm/ClassInheritance.mls +++ b/hkmc2/shared/src/test/mlscript/wasm/ClassInheritance.mls @@ -257,13 +257,13 @@ class B //│ (local.get $this))) //│ (return //│ (local.get $this)))) -//│ (func $Unit_init1 (type $Unit_init) (param $this (ref null any)) (result (ref null any)) +//│ (func $Unit_init (export "Unit_init") (type $Unit_init) (param $this (ref null any)) (result (ref null any)) //│ (block (result (ref null any)) //│ (nop) //│ (nop) //│ (return //│ (local.get $this)))) -//│ (func $Unit_ctor1 (type $Unit_ctor) (result (ref null any)) +//│ (func $Unit_ctor (export "Unit_ctor") (type $Unit_ctor) (result (ref null any)) //│ (local $this (ref null any)) //│ (block (result (ref null any)) //│ (local.set $this @@ -273,7 +273,7 @@ class B //│ (local.get $this)) //│ (i32.const 3)) //│ (drop -//│ (call $Unit_init1 +//│ (call $Unit_init //│ (local.get $this))) //│ (return //│ (local.get $this)))) @@ -281,7 +281,7 @@ class B //│ (block //│ (global.set $Unit$inst //│ (ref.cast (ref null $Unit) -//│ (call $Unit_ctor1))))) +//│ (call $Unit_ctor))))) //│ (func $entry (export "entry") (type $entry1) (result (ref null any)) //│ (block (result (ref null any)) //│ (block @@ -294,8 +294,8 @@ class B //│ (elem $B_init declare func $B_init) //│ (elem $A_ctor declare func $A_ctor) //│ (elem $B_ctor declare func $B_ctor) -//│ (elem $Unit_init1 declare func $Unit_init1) -//│ (elem $Unit_ctor1 declare func $Unit_ctor1) +//│ (elem $Unit_init declare func $Unit_init) +//│ (elem $Unit_ctor declare func $Unit_ctor) //│ (elem $start1 declare func $start1) //│ (elem $entry declare func $entry) //│ (start $start1)) diff --git a/hkmc2/shared/src/test/mlscript/wasm/ControlFlow.mls b/hkmc2/shared/src/test/mlscript/wasm/ControlFlow.mls index 6aaa50dbdc..d7710a7b84 100644 --- a/hkmc2/shared/src/test/mlscript/wasm/ControlFlow.mls +++ b/hkmc2/shared/src/test/mlscript/wasm/ControlFlow.mls @@ -25,13 +25,13 @@ let i = 0 in //│ (import "system" "lt_impl" (func $lt_impl (type $lt_impl))) //│ (import "system" "plus_impl" (func $plus_impl (type $plus_impl))) //│ (global $Unit$inst (export "Unit$inst") (mut (ref null $Unit)) (ref.null $Unit)) -//│ (func $Unit_init1 (type $Unit_init) (param $this (ref null any)) (result (ref null any)) +//│ (func $Unit_init (export "Unit_init") (type $Unit_init) (param $this (ref null any)) (result (ref null any)) //│ (block (result (ref null any)) //│ (nop) //│ (nop) //│ (return //│ (local.get $this)))) -//│ (func $Unit_ctor1 (type $Unit_ctor) (result (ref null any)) +//│ (func $Unit_ctor (export "Unit_ctor") (type $Unit_ctor) (result (ref null any)) //│ (local $this (ref null any)) //│ (block (result (ref null any)) //│ (local.set $this @@ -41,7 +41,7 @@ let i = 0 in //│ (local.get $this)) //│ (i32.const 1)) //│ (drop -//│ (call $Unit_init1 +//│ (call $Unit_init //│ (local.get $this))) //│ (return //│ (local.get $this)))) @@ -49,7 +49,7 @@ let i = 0 in //│ (block //│ (global.set $Unit$inst //│ (ref.cast (ref null $Unit) -//│ (call $Unit_ctor1))))) +//│ (call $Unit_ctor))))) //│ (func $entry (export "entry") (type $entry1) (result (ref null any)) //│ (local $i (ref null any)) //│ (local $scrut (ref null any)) @@ -101,8 +101,8 @@ let i = 0 in //│ (global.get $Unit$inst)))) //│ (local.get $matchRes)))))) //│ (local.get $i)))) -//│ (elem $Unit_init1 declare func $Unit_init1) -//│ (elem $Unit_ctor1 declare func $Unit_ctor1) +//│ (elem $Unit_init declare func $Unit_init) +//│ (elem $Unit_ctor declare func $Unit_ctor) //│ (elem $start1 declare func $start1) //│ (elem $entry declare func $entry) //│ (start $start1)) diff --git a/hkmc2/shared/src/test/mlscript/wasm/ReplImports.mls b/hkmc2/shared/src/test/mlscript/wasm/ReplImports.mls index 0c1d18e98f..6422affdbb 100644 --- a/hkmc2/shared/src/test/mlscript/wasm/ReplImports.mls +++ b/hkmc2/shared/src/test/mlscript/wasm/ReplImports.mls @@ -12,7 +12,22 @@ let unit = () //│ Wasm result: //│ = {} +:wat inc(Foo.x) +//│ Wat: +//│ (module +//│ (type $Object (sub (struct (field $$tag (mut i32))))) +//│ (type $Foo (sub $Object (struct (field $$tag (mut i32)) (field $x (mut (ref null any)))))) +//│ (type $inc (func (param $x (ref null any)) (result (ref null any)))) +//│ (type $entry1 (func (result (ref null any)))) +//│ (import "repl" "inc" (func $inc (type $inc))) +//│ (import "repl" "Foo$inst" (global $Foo (mut (ref null $Foo)))) +//│ (func $entry (export "entry") (type $entry1) (result (ref null any)) +//│ (call $inc +//│ (struct.get $Foo $x +//│ (ref.cast (ref $Foo) +//│ (global.get $Foo))))) +//│ (elem $entry declare func $entry)) //│ Wasm result: //│ = 2 @@ -56,7 +71,11 @@ unit //│ (module //│ (type $Object (sub (struct (field $$tag (mut i32))))) //│ (type $Unit (sub $Object (struct (field $$tag (mut i32))))) +//│ (type $Unit_init (func (param $this (ref null any)) (result (ref null any)))) +//│ (type $Unit_ctor (func (result (ref null any)))) //│ (type $entry1 (func (result (ref null any)))) +//│ (import "repl" "Unit_init" (func $Unit_init (type $Unit_init))) +//│ (import "repl" "Unit_ctor" (func $Unit (type $Unit_ctor))) //│ (import "repl" "Unit$inst" (global $Unit (mut (ref null $Unit)))) //│ (func $entry (export "entry") (type $entry1) (result (ref null any)) //│ (global.get $Unit)) diff --git a/hkmc2/shared/src/test/mlscript/wasm/SingletonUnit.mls b/hkmc2/shared/src/test/mlscript/wasm/SingletonUnit.mls index 51edfc3a78..23fc55ad78 100644 --- a/hkmc2/shared/src/test/mlscript/wasm/SingletonUnit.mls +++ b/hkmc2/shared/src/test/mlscript/wasm/SingletonUnit.mls @@ -13,13 +13,13 @@ //│ (type $entry1 (func (result (ref null any)))) //│ (type $start (func)) //│ (global $Unit$inst (export "Unit$inst") (mut (ref null $Unit)) (ref.null $Unit)) -//│ (func $Unit_init1 (type $Unit_init) (param $this (ref null any)) (result (ref null any)) +//│ (func $Unit_init (export "Unit_init") (type $Unit_init) (param $this (ref null any)) (result (ref null any)) //│ (block (result (ref null any)) //│ (nop) //│ (nop) //│ (return //│ (local.get $this)))) -//│ (func $Unit_ctor1 (type $Unit_ctor) (result (ref null any)) +//│ (func $Unit_ctor (export "Unit_ctor") (type $Unit_ctor) (result (ref null any)) //│ (local $this (ref null any)) //│ (block (result (ref null any)) //│ (local.set $this @@ -29,7 +29,7 @@ //│ (local.get $this)) //│ (i32.const 1)) //│ (drop -//│ (call $Unit_init1 +//│ (call $Unit_init //│ (local.get $this))) //│ (return //│ (local.get $this)))) @@ -37,11 +37,11 @@ //│ (block //│ (global.set $Unit$inst //│ (ref.cast (ref null $Unit) -//│ (call $Unit_ctor1))))) +//│ (call $Unit_ctor))))) //│ (func $entry (export "entry") (type $entry1) (result (ref null any)) //│ (global.get $Unit$inst)) -//│ (elem $Unit_init1 declare func $Unit_init1) -//│ (elem $Unit_ctor1 declare func $Unit_ctor1) +//│ (elem $Unit_init declare func $Unit_init) +//│ (elem $Unit_ctor declare func $Unit_ctor) //│ (elem $start1 declare func $start1) //│ (elem $entry declare func $entry) //│ (start $start1)) From e5c80d41c0748b92cbc07b0b7e26cb93d59e24cb Mon Sep 17 00:00:00 2001 From: FetBoba Date: Sat, 2 May 2026 23:24:40 +0800 Subject: [PATCH 10/12] Restored empty lines --- hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala | 2 ++ hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala | 2 ++ 2 files changed, 4 insertions(+) diff --git a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala index e12707516a..54da97245c 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala @@ -125,3 +125,5 @@ trait CompilerCache: end CompilerCache + + diff --git a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala index 92d41d7bd3..6584dd5a18 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala @@ -390,4 +390,6 @@ class MLsCompiler case CompilationTarget.Wasm => emitWasm(file, le_2, exportedSymbol, preservedSymbols, newCtx, mutable.Map.empty) + end MLsCompiler + From 83da0e527035755cc172315662ea55adbcefdca7 Mon Sep 17 00:00:00 2001 From: FetBoba Date: Fri, 15 May 2026 17:59:01 +0800 Subject: [PATCH 11/12] Added whitespaces and removed redundant wat --- .../src/main/scala/hkmc2/MLsCompiler.scala | 18 +++++------------- .../scala/hkmc2/codegen/wasm/text/Ctx.scala | 2 +- .../src/test/mlscript/wasm/ReplImports.mls | 18 ------------------ 3 files changed, 6 insertions(+), 32 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala index a18e4a8212..1fb219b38f 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala @@ -57,15 +57,13 @@ class MLsCompiler (paths: MLsCompiler.Paths, mkRaise: io.Path => Raise) (using cctx: CompilerCtx, config: Config): import paths.* - - // TODO adapt logic + given DebugPrinter = new DebugPrinter val etl = new TraceLogger{override def doTrace: Bool = false} val ltl = new TraceLogger{override def doTrace: Bool = false} - // val ltl = new TraceLogger{override def doTrace: Bool = true} val rtl = new TraceLogger{override def doTrace: Bool = false} - - + + var dbgParsing = false var dbgElab = false @@ -338,7 +336,6 @@ class MLsCompiler given Elaborator.State = new Elaborator.State: override def dbg: Bool = dbgElab - // TODO adapt logic given SymbolPrinter = new SymbolPrinter( Scope.empty(Scope.Cfg.default.copy( escapeChars = false, @@ -346,11 +343,6 @@ class MLsCompiler includeZero = true, )) ) - val etl = new TraceLogger{override def doTrace: Bool = false} - val ltl = new TraceLogger{override def doTrace: Bool = false} - // val ltl = new TraceLogger{override def doTrace: Bool = true} - val rtl = new TraceLogger{override def doTrace: Bool = false} - val preludeParse = ParserSetup(preludeFile, dbgParsing) val mainParse = ParserSetup(file, dbgParsing) @@ -402,7 +394,7 @@ class MLsCompiler emitJs(file, wd, le_2, exportedSymbol) case CompilationTarget.Wasm => emitWasm(file, le_2, exportedSymbol, preservedSymbols, newCtx, mutable.Map.empty) - - + + end MLsCompiler diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/Ctx.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/Ctx.scala index d596579b46..51e6a8e91b 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/Ctx.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/Ctx.scala @@ -798,7 +798,7 @@ class Ctx(using State) extends ToWat: /** Checks whether the global variable scope contains the variable `sym`. */ def containsGlobal(sym: Symbol): Bool = namedGlobals.contains(sym) - + /** Returns all globals in this context. */ def getGlobals: Seq[Symbol] = namedGlobals.keys.toSeq diff --git a/hkmc2/shared/src/test/mlscript/wasm/ReplImports.mls b/hkmc2/shared/src/test/mlscript/wasm/ReplImports.mls index 9f1d043752..7aeba3bd93 100644 --- a/hkmc2/shared/src/test/mlscript/wasm/ReplImports.mls +++ b/hkmc2/shared/src/test/mlscript/wasm/ReplImports.mls @@ -12,25 +12,7 @@ let unit = () //│ Wasm result: //│ = {} -:wat inc(Foo.x) -//│ Wat: -//│ (module -//│ (type $TypeInfoBase (sub (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) -//│ (type $Object (sub (struct (field $$typeinfo (mut (ref $TypeInfoBase)))))) -//│ (type $Foo (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $x (mut (ref null any)))))) -//│ (type $inc (func (param $x (ref null any)) (result (ref null any)))) -//│ (type $Foo_typeinfo (sub $TypeInfoBase (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) -//│ (type $entry (func (result (ref null any)))) -//│ (import "repl" "inc" (func $inc (type $inc))) -//│ (import "repl" "Foo_typeinfo" (global $Foo_typeinfo (ref $Foo_typeinfo))) -//│ (import "repl" "Foo$inst" (global $Foo (mut (ref null $Foo)))) -//│ (func $entry (export "entry") (type $entry) (result (ref null any)) -//│ (call $inc -//│ (struct.get $Foo $x -//│ (ref.cast (ref $Foo) -//│ (global.get $Foo))))) -//│ (elem $entry declare func $entry)) //│ Wasm result: //│ = 2 From 239ca2b60538ec8dde74f2e50f0ba7af98fdeb3e Mon Sep 17 00:00:00 2001 From: FetBoba Date: Sat, 16 May 2026 13:35:45 +0800 Subject: [PATCH 12/12] Changed mjs import test to mls --- hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala | 3 ++- .../src/test/mlscript/codegen/ImportMLsJS.mls | 13 +------------ 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala index 1fb219b38f..04d1bbd345 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala @@ -66,7 +66,8 @@ class MLsCompiler var dbgParsing = false var dbgElab = false - + + /** Symbols a module wants preserved through optimization passes and surfaced to * downstream importers: the exported module itself, its members, and their type symbols. */ private def preservedSymbolsFor(exportedSymbol: Opt[BlockMemberSymbol]): Set[codegen.Local] = diff --git a/hkmc2/shared/src/test/mlscript/codegen/ImportMLsJS.mls b/hkmc2/shared/src/test/mlscript/codegen/ImportMLsJS.mls index 289e60553a..9ec9535106 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/ImportMLsJS.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/ImportMLsJS.mls @@ -1,21 +1,10 @@ :js -import "../../mlscript-compile/Option.mjs" -//│ Option = class Option { -//│ Some: fun Some { class: class Some }, -//│ None: None, -//│ Both: fun Both { class: class Both }, -//│ unsafe: class unsafe -//│ } +import "../../mlscript-compile/Option.mls" -:e Option.isDefined(new Option.Some(1)) -//│ ╔══[COMPILATION ERROR] Expected a statically known class; found selection. -//│ ║ l.14: Option.isDefined(new Option.Some(1)) -//│ ║ ^^^^^^^^^^^ -//│ ╙── The 'new' keyword requires a statically known class; use the 'new!' operator for dynamic instantiation. //│ = true Option.isDefined(new! Option.Some(1))