diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index be432804ee..0c2bed9d8d 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -10,6 +10,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + with: + fetch-depth: 0 - name: Install Nix uses: DeterminateSystems/nix-installer-action@main - uses: rrbutani/use-nix-shell-action@v1 @@ -30,3 +32,37 @@ jobs: - name: Fail if tests failed if: steps.run_test.outcome == 'failure' run: exit 1 + - name: Check Web IDE deploy changes + if: github.event_name == 'push' && github.ref == 'refs/heads/hkmc2' + id: web_ide_changes + run: | + before="${{ github.event.before }}" + after="${{ github.sha }}" + if [ "$before" = "0000000000000000000000000000000000000000" ]; then + changed=true + elif git diff --quiet "$before" "$after" -- wrangler.toml hkmc2/shared/src/test/mlscript-packages/web-ide/; then + changed=false + else + diff_status=$? + if [ "$diff_status" -eq 1 ]; then + changed=true + else + exit "$diff_status" + fi + fi + echo "changed=${changed}" >> "$GITHUB_OUTPUT" + - name: Build Web IDE deploy assets + if: github.event_name == 'push' && github.ref == 'refs/heads/hkmc2' && steps.web_ide_changes.outputs.changed == 'true' + run: | + sbt -J-Xmx4096M -J-Xss8M hkmc2JS/fullOptJS + rm -rf hkmc2/shared/src/test/mlscript-packages/web-ide/build + mkdir -p hkmc2/shared/src/test/mlscript-packages/web-ide/build + cp -R hkmc2/js/target/scala-*/hkmc2-opt/. hkmc2/shared/src/test/mlscript-packages/web-ide/build/ + - name: Deploy Web IDE to Cloudflare Workers + if: github.event_name == 'push' && github.ref == 'refs/heads/hkmc2' && steps.web_ide_changes.outputs.changed == 'true' + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + wranglerVersion: "4.29.0" + command: deploy --config wrangler.toml diff --git a/.github/workflows/web-ide-preview.yml b/.github/workflows/web-ide-preview.yml new file mode 100644 index 0000000000..65c94d7f84 --- /dev/null +++ b/.github/workflows/web-ide-preview.yml @@ -0,0 +1,79 @@ +name: Web IDE Preview + +on: + push: + branches-ignore: + - hkmc2 + paths: + - .github/workflows/web-ide-preview.yml + - wrangler.toml + - hkmc2/shared/src/test/mlscript-packages/web-ide/** + +jobs: + preview: + runs-on: ubuntu-latest + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + steps: + - uses: actions/checkout@v3 + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@main + - uses: rrbutani/use-nix-shell-action@v1 + with: + devShell: .#default + - name: Install npm dependencies (TypeScript, Binaryen, etc.) + run: npm ci + - name: Run Web IDE package test + run: | + sbt -J-Xmx4096M -J-Xss8M hkmc2JVM/test "hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide" + - name: Check no changes + run: | + git update-index -q --refresh + git diff-files -p --exit-code + - name: Build Web IDE deploy assets + run: | + sbt -J-Xmx4096M -J-Xss8M hkmc2JS/fullOptJS + rm -rf hkmc2/shared/src/test/mlscript-packages/web-ide/build + mkdir -p hkmc2/shared/src/test/mlscript-packages/web-ide/build + cp -R hkmc2/js/target/scala-*/hkmc2-opt/. hkmc2/shared/src/test/mlscript-packages/web-ide/build/ + - name: Compute preview alias + id: preview_alias + run: | + node <<'NODE' + const fs = require("fs"); + const workerName = "mlscript-web-ide"; + const maxAliasLength = 63 - workerName.length - 1; + const branchName = process.env.GITHUB_REF_NAME || "preview"; + let alias = branchName + .toLowerCase() + .replace(/[^a-z0-9-]+/g, "-") + .replace(/-+/g, "-") + .replace(/^-+|-+$/g, ""); + if (!alias) alias = "preview"; + if (!/^[a-z]/.test(alias)) alias = `branch-${alias}`; + if (alias.length > maxAliasLength) { + alias = alias.slice(0, maxAliasLength).replace(/-+$/g, ""); + } + fs.appendFileSync(process.env.GITHUB_OUTPUT, `alias=${alias}\n`); + console.log(`Preview alias: ${alias}`); + NODE + - name: Enable Web IDE preview URLs + if: env.CLOUDFLARE_API_TOKEN != '' && env.CLOUDFLARE_ACCOUNT_ID != '' + run: | + curl --fail -X POST \ + -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \ + -H "Content-Type: application/json" \ + --data '{"enabled":false,"previews_enabled":true}' \ + "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/workers/scripts/mlscript-web-ide/subdomain" + - name: Upload Web IDE preview to Cloudflare Workers + if: env.CLOUDFLARE_API_TOKEN != '' && env.CLOUDFLARE_ACCOUNT_ID != '' + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + wranglerVersion: "4.29.0" + command: versions upload --preview-alias ${{ steps.preview_alias.outputs.alias }} --config wrangler.toml + - name: Skip Web IDE preview upload + if: env.CLOUDFLARE_API_TOKEN == '' || env.CLOUDFLARE_ACCOUNT_ID == '' + run: echo "Cloudflare preview secrets are not configured; skipping preview upload." diff --git a/README.md b/README.md index 7aaba9b10c..548b9f4999 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,8 @@ and then use one of the following commands. - `hkmc2DiffTests/test` for running only the main diff-tests, in `hkmc2/shared/src/test/mlscript`. - `hkmc2MainTests/test` for running the above two. - `hkmc2AppsTests/test` for running the applications compile and diff tests. +- `hkmc2PackagesTest/test` for compiling the packages in `hkmc2/shared/src/test/mlscript-packages`. + Each folder represents a package, which can be imported directly using its name. - `hkmc2NofibTests/test` for running the nofib compile and diff tests. - `hkmc2WasmTests/test` for running the wasm compile and diff tests. - `hkmc2MostTests/test` for running all of the above. @@ -248,4 +250,3 @@ Links to the individual paper artifact repositories are provided at the correspo - diff --git a/build.sbt b/build.sbt index d7e9065fda..2861b170b5 100644 --- a/build.sbt +++ b/build.sbt @@ -61,6 +61,49 @@ lazy val hkmc2 = crossProject(JSPlatform, JVMPlatform).in(file("hkmc2")) _.withModuleKind(ModuleKind.ESModule) .withOutputPatterns(OutputPatterns.fromJSFile("MLscript.mjs")) }, + Compile / sourceGenerators += Def.task { + val rootDir = (ThisBuild / baseDirectory).value + val stdDir = rootDir / "hkmc2" / "shared" / "src" / "test" / "mlscript-compile" + val declsDir = rootDir / "hkmc2" / "shared" / "src" / "test" / "mlscript" / "decls" + val out = (Compile / sourceManaged).value / "hkmc2" / "WebIDEStd.scala" + val preludeFile = declsDir / "Prelude.mls" + val stdFiles = ((stdDir * "*.mls") +++ (stdDir * "*.mjs") +++ (stdDir / "quotes" * "*.mls") +++ (stdDir / "quotes" * "*.mjs")).get + .filterNot(_.getName == "Prelude.mls") + .sortBy(file => stdDir.toPath.relativize(file.toPath).toString) + + def scalaString(value: String): String = + "\"" + value.flatMap { + case '\\' => "\\\\" + case '"' => "\\\"" + case '\n' => "\\n" + case '\r' => "\\r" + case '\t' => "\\t" + case c if c.isControl => f"\\u${c.toInt}%04x" + case c => c.toString + } + "\"" + + val entries = stdFiles.map { file => + val relativePath = stdDir.toPath.relativize(file.toPath).toString.replace(java.io.File.separatorChar, '/') + s"""js.Array(${scalaString("/std/" + relativePath)}, ${scalaString(IO.read(file))})""" + } + val source = + s"""|package hkmc2 + | + |import scala.scalajs.js + |import scala.scalajs.js.annotation.JSExportTopLevel + | + |object WebIDEStd: + | @JSExportTopLevel("std") + | val std: js.Dynamic = js.Dynamic.literal( + | prelude = ${scalaString(IO.read(preludeFile))}, + | files = js.Array( + | ${entries.mkString(",\n ")} + | ) + | ) + |""".stripMargin + IO.write(out, source) + Seq(out) + }.taskValue, libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "2.2.0", ) .dependsOn(core) @@ -112,6 +155,20 @@ lazy val hkmc2NofibTests = hkmc2TestSubproject("hkmc2NofibTests", Some("NofibCom lazy val hkmc2AppsTests = hkmc2TestSubproject("hkmc2AppsTests", Some("AppsCompileTestRunner"), "AppsDiffTestRunner") lazy val hkmc2WasmTests = hkmc2TestSubproject("hkmc2WasmTests", Some("WasmCompileTestRunner"), "WasmDiffTestRunner") +lazy val hkmc2PackagesTest = project.in(file("hkmc2PackagesTest")) + .dependsOn(hkmc2JVM % "compile->compile;test->test") + .settings( + scalaVersion := scala3Version, + + libraryDependencies += "org.scalactic" %%% "scalactic" % scalaTestVersion, + libraryDependencies += "org.scalatest" %%% "scalatest" % scalaTestVersion % "test", + libraryDependencies += "com.lihaoyi" %% "ujson" % "4.4.3", + + Test / test := (Test / testOnly).toTask(" hkmc2.PackageTestRunner").value, + + Test/run/fork := true, // so that CTRL+C actually terminates the watcher + ) + lazy val hkmc2MainTests = project.in(file("hkmc2MainTests")) .settings( Test / test := ( @@ -126,6 +183,7 @@ lazy val hkmc2MostTests = project.in(file("hkmc2MostTests")) (hkmc2DiffTests / Test / test) .dependsOn(hkmc2NofibTests / Test / test) .dependsOn(hkmc2AppsTests / Test / test) + .dependsOn(hkmc2PackagesTest / Test / test) .dependsOn(hkmc2WasmTests / Test / test) .dependsOn(hkmc2JVM / Test / test) ).value @@ -133,12 +191,13 @@ lazy val hkmc2MostTests = project.in(file("hkmc2MostTests")) lazy val hkmc2AllTests = project.in(file("hkmc2AllTests")) .settings( - Test / test := ( + Test / test := Def.sequential( + hkmc2JVM / Test / test, // prepares compile-test `.mjs` outputs used by JS tests (hkmc2DiffTests / Test / test) .dependsOn(hkmc2NofibTests / Test / test) .dependsOn(hkmc2AppsTests / Test / test) + .dependsOn(hkmc2PackagesTest / Test / test) .dependsOn(hkmc2WasmTests / Test / test) - .dependsOn(hkmc2JVM / Test / test) .dependsOn(hkmc2JS / Test / test) .dependsOn(hkmc2Benchmarks / Test / compile) ).value diff --git a/docs/idomatic-mlscript-rules.md b/docs/idomatic-mlscript-rules.md new file mode 100644 index 0000000000..58867d50ca --- /dev/null +++ b/docs/idomatic-mlscript-rules.md @@ -0,0 +1,598 @@ +# Idomatic MLscript Rules + +## List of Rules and Progress Tracker + +- [x] Use object literals instead of manual field assignments + - [x] Empty mutable objects + - [x] Mutable objects with fields +- [x] Utilize the Ultimate Conditional Syntax + - [x] Equality tests + - [x] Get rid of nested `if` using `and` + - [x] Saves a level of indentation + - [x] Combine the techniques together + - [x] Factor repeated scrutinees inside flat UCS + - [x] Reorganize complementary patterns + - [x] Avoid redundant wildcard arguments in negated constructor patterns + - [x] Use `~Absent ... do` for optional effects + - [x] Use `else` or variable patterns for plain complement fallback +- [x] Get rid of parenthesis of function calls using `of` keywords +- [x] Organize consecutive `let` bindings using splits +- [x] Prefer `not` over `is false` +- [x] Use `do` instead of `then ... else ()` +- [x] Prefer quoted identifiers for symbol-like fields and values +- [x] Prefer quoted field selection for keyword-like fields +- [x] Drop braces from multiline object literals +- [x] Drop unnecessary end-of-line commas +- [x] Drop unnecessary parentheses around selections +- [x] Do not overuse `of` + +## Rules + +### Use object literals instead of manual field assignments + +#### Case 1: Empty mutable objects + +`new mut Object` can be rewritten as `mut {}`. + +#### Case 2: Mutable objects with fields. + +Before: + +```mlscript + fun bubbleEventOptions(detail) = + let options = new mut Object + set + options.bubbles = true + options.detail = detail + options +``` + +After: + +```mlscript + fun bubbleEventOptions(detail) = + mut { bubbles: true, :detail } +``` + +### Utilize the Ultimate Conditional Syntax + +#### Case 1: Equality Tests + +Before: + +``` + fun shouldHandleFsEvent(kind) = + if + kind === "write" then true + kind === "delete" then true + kind === "rename" then true + kind === "readonly" then true + kind === "attr" then true + else false +``` + +Step 1: Use `is` when the RHS can be written as a pattern. + +``` + fun shouldHandleFsEvent(kind) = + if + kind is "write" then true + kind is "delete" then true + kind is "rename" then true + kind is "readonly" then true + kind is "attr" then true + else false +``` + +Step 2: Use disjunctive patterns. + +``` + fun shouldHandleFsEvent(kind) = + if + kind is "write" | "delete" | "rename" | "readonly" | "attr" then true + else false +``` + +Step 3: Use shorthand UCS expression when it is only a test. + +``` + fun shouldHandleFsEvent(kind) = + kind is "write" | "delete" | "rename" | "readonly" | "attr" +``` + +#### Get rid of nested `if` using `and` + +Before: + +```mlscript + if existingTabId is Absent do + if entry.at(1).path === filePath do + set existingTabId = entry.at(0) +``` + +Step 1: Remove nested `if` using `and`. + +```mlscript + if existingTabId is Absent and + entry.at(1).path === filePath do + set existingTabId = entry.at(0) +``` + +Step 2: Put conditions on the same line to save space. + +```mlscript + if existingTabId is Absent and entry.at(1).path === filePath do + set existingTabId = entry.at(0) +``` + +#### Case 3: Saves a level of indentation + +Before: The body of the function is a Ultimate Conditional Syntax expression. + +```mlscript + fun showArrow(arrow) = + if arrow.classList.contains("visible") is false do + set arrow.style.display = "flex" + arrow.offsetHeight + arrow.classList.add("visible") +``` + +After: We can begin `if` at the same line of the function. + +```mlscript + fun showArrow(arrow) = if arrow.classList.contains("visible") is false do + set arrow.style.display = "flex" + arrow.offsetHeight + arrow.classList.add("visible") +``` + +#### Case 4: Combine the techniques together + +Before: + +```mlscript + fun handleFsEventForTab(tabId, tab, event) = + if tab.path === event.path do + let kind = event.("type") + if + kind === "delete" then this.closeTab(tabId) + kind === "rename" then + set + tab.name = event.node.name + tab.path = event.newPath + this.updateDisplay() + kind === "readonly" then + set tab.readonly = event.readonly + this.updateDisplay() + kind === "attr" then + set tab.attrs = nodeAttrs(event) + this.updateDisplay() + kind === "write" then updateTabContent(tab, event.path) + else () +``` + +Step 1: Use `is` when possible + +```mlscript + fun handleFsEventForTab(tabId, tab, event) = + if tab.path === event.path do + let kind = event.("type") + if + kind is "delete" then this.closeTab(tabId) + kind is "rename" then + set + tab.name = event.node.name + tab.path = event.newPath + this.updateDisplay() + kind is "readonly" then + set tab.readonly = event.readonly + this.updateDisplay() + kind is "attr" then + set tab.attrs = nodeAttrs(event) + this.updateDisplay() + kind is "write" then updateTabContent(tab, event.path) + else () +``` + +Step 2: Split after `is` when the prefix is the same. + +```mlscript + fun handleFsEventForTab(tabId, tab, event) = + if tab.path === event.path do + let kind = event.("type") + if kind is + "delete" then this.closeTab(tabId) + "rename" then + set + tab.name = event.node.name + tab.path = event.newPath + this.updateDisplay() + "readonly" then + set tab.readonly = event.readonly + this.updateDisplay() + "attr" then + set tab.attrs = nodeAttrs(event) + this.updateDisplay() + "write" then updateTabContent(tab, event.path) + else () +``` + +Step 3: Merge nested `if` using `and`. + +```mlscript + fun handleFsEventForTab(tabId, tab, event) = + if tab.path === event.path and + let kind = event.("type") + kind is + "delete" then this.closeTab(tabId) + "rename" then + set + tab.name = event.node.name + tab.path = event.newPath + this.updateDisplay() + "readonly" then + set tab.readonly = event.readonly + this.updateDisplay() + "attr" then + set tab.attrs = nodeAttrs(event) + this.updateDisplay() + "write" then updateTabContent(tab, event.path) + else () +``` + +Step 4: The scrutinee can be an expression. + +```mlscript + fun handleFsEventForTab(tabId, tab, event) = + if tab.path === event.path and event.("type") is + "delete" then this.closeTab(tabId) + "rename" then + set + tab.name = event.node.name + tab.path = event.newPath + this.updateDisplay() + "readonly" then + set tab.readonly = event.readonly + this.updateDisplay() + "attr" then + set tab.attrs = nodeAttrs(event) + this.updateDisplay() + "write" then updateTabContent(tab, event.path) + else () +``` + +#### Case 5: Factor repeated scrutinees inside flat UCS + +Before: + +```mlscript + fun select(x, y) = + if + x is A then a + x is B then b + y is C then c + y is D then d + else e +``` + +After: + +```mlscript + fun select(x, y) = if + x is + A then a + B then b + y is + C then c + D then d + else e +``` + +1. If the whole function body is a UCS expression, begin the `if` on the function definition line. +2. When multiple branches test the same scrutinee with `is`, split after `is` and group the corresponding patterns under that scrutinee. +3. Keep the result as one flat UCS expression. Do not introduce nested `if` expressions for later scrutinees; UCS can switch scrutinees in the same conditional. + +#### Case 6: Reorganize complementary patterns + +Before: + +```mlscript + fun fromMaybe(x) = if x is + Some(value) then value + ~Some(_) then default +``` + +After: + +```mlscript + fun fromMaybe(x) = if x is + Some(value) then value + _ then default +``` + +When the fallback branch needs the value: + +```mlscript + fun handle(x) = if x is + Absent then missing() + value then present(value) +``` + +1. When you see opposite patterns such as `P` and `~P`, recognize that they partition the cases. +2. Reorganize the match around the positive pattern and the remaining cases instead of mechanically preserving the negated pattern. +3. If the remaining branch needs the value, bind it with a variable pattern. A variable pattern matches the remaining value and gives it a local name. + +### Get rid of parenthesis of function calls using `of` keywords + +Before: There is a single `)` on the newline in the middle. + +```mlscript + fun setupEventListeners() = + let panel = this + window.addEventListener("file-open", event => + panel.openFile(event.detail.path, event.detail.fileName) + ) + document.addEventListener("keydown", event => panel.handleShortcut(event)) +``` + +After: Use `of`, which behaves like Haskell's `$`, but supports multiple arguments. + +```mlscript + fun setupEventListeners() = + let panel = this + window.addEventListener of "file-open", event => + panel.openFile(event.detail.path, event.detail.fileName) + document.addEventListener of "keydown", event => panel.handleShortcut(event) +``` + +### Organize consecutive `let` bindings using splits + +Before: + +```mlscript + let tabBar = this.querySelector(".tab-bar") + let leftArrow = this.querySelector(".tab-scroll-left") + let rightArrow = this.querySelector(".tab-scroll-right") + let scrollInterval = null + let scrollSpeed = 3 +``` + +After: + +```mlscript + let + tabBar = this.querySelector(".tab-bar") + leftArrow = this.querySelector(".tab-scroll-left") + rightArrow = this.querySelector(".tab-scroll-right") + scrollInterval = null + scrollSpeed = 3 +``` + +### Additional rewrite patterns from PR review + +#### Prefer `not` over `is false` + +Use `not X` for boolean negation. + +Before: + +```mlscript + if arrow.classList.contains("visible") is false do + set arrow.style.display = "flex" +``` + +After: + +```mlscript + if not arrow.classList.contains("visible") do + set arrow.style.display = "flex" +``` + +#### Use `do` instead of `then ... else ()` + +When the conditional only performs an action and the `else` branch is `()`, +make it a `do` conditional. + +Before: + +```mlscript + if shouldUpdate then + this.updateDisplay() + else () +``` + +After: + +```mlscript + if shouldUpdate do + this.updateDisplay() +``` + +#### Prefer quoted identifiers for symbol-like fields and values + +When both an object field name and its value are symbol-like strings, use quoted +identifiers instead of string literals. + +Before: + +```mlscript + mut { "type": "module" } +``` + +After: + +```mlscript + mut { 'type: 'module } +``` + +#### Drop braces from multiline object literals + +When an object literal spans multiple lines, indentation can delimit the fields. + +Before: + +```mlscript + mut { + 'type: 'compile-success + id: id + changes: changes + } +``` + +After: + +```mlscript + mut + 'type: 'compile-success + id: id + changes: changes +``` + +#### Do not overuse `of` + +Use `of` when it removes noisy parentheses in a multiline or nested call. For +simple flat calls, ordinary parentheses are clearer. + +Before: + +```mlscript + window.setTimeout of hideLater, 200 +``` + +After: + +```mlscript + window.setTimeout(hideLater, 200) +``` + +#### Avoid redundant wildcard arguments in negated constructor patterns + +When a negated constructor pattern only tests that a value is not built by that +constructor, omit unused wildcard arguments. + +Before: + +```mlscript + fun fromMaybe(x) = if x is + Some(value) then value + ~Some(_) then default +``` + +After: + +```mlscript + fun fromMaybe(x) = if x is + Some(value) then value + ~Some then default +``` + +#### Use `~Absent ... do` for optional effects + +When one branch of a conditional is `Absent then ()` and the other branch only +performs an effect, keep only the present case and use `do`. + +Before: + +```mlscript + if this.unsubscribe is + Absent then () + unsubscribe then unsubscribe() +``` + +After: + +```mlscript + if this.unsubscribe is ~Absent as unsubscribe do + unsubscribe() +``` + +#### Prefer quoted field selection for keyword-like fields + +When selecting a field whose name is keyword-like, prefer quoted field selection +over string-indexed selection. + +Before: + +```mlscript + event.("type") +``` + +After: + +```mlscript + event.'type +``` + +#### Drop unnecessary end-of-line commas + +When indentation already separates flat entries and no nested block is being +introduced, omit end-of-line commas. + +Before: + +```mlscript + let divisions = [ + (amount: 60, unit: "seconds"), + (amount: 60, unit: "minutes"), + ] +``` + +After: + +```mlscript + let divisions = [ + (amount: 60, unit: "seconds") + (amount: 60, unit: "minutes") + ] +``` + +#### Drop unnecessary parentheses around selections + +Do not wrap simple field selections in parentheses unless precedence requires +it. + +Before: + +```mlscript + mut + startLine: (startPos.line) + startColumn: (startPos.column) +``` + +After: + +```mlscript + mut + startLine: startPos.line + startColumn: startPos.column +``` + +#### Use `else` or variable patterns for plain complement fallback + +When a second pattern is only the complement of the first and does not add a +meaningful test, use `else` or a variable pattern instead of spelling out the +negated pattern. + +Before: + +```mlscript + fun childrenOf(node) = if node.children is + Absent then [] + ~Absent as children then children +``` + +After: + +```mlscript + fun childrenOf(node) = if node.children is + Absent then [] + else node.children +``` + +If the fallback branch needs a local name, bind it directly: + +```mlscript + fun childrenOf(node) = if node.children is + Absent then [] + children then children +``` diff --git a/docs/web-ide-mlscript-rewrite-progress.md b/docs/web-ide-mlscript-rewrite-progress.md new file mode 100644 index 0000000000..25137d19e8 --- /dev/null +++ b/docs/web-ide-mlscript-rewrite-progress.md @@ -0,0 +1,47 @@ +# Web IDE MLscript Rewrite Progress + +This tracker follows `docs/web-ide-mlscript-rewrite-workflow.md`. + +## Remaining Hand-Written JavaScript + +- [x] `components/PanelPersistence.js` -> `components/PanelPersistence.mls` +- [x] `components/ResizeHandle.js` -> `components/ResizeHandle.mls` +- [x] `components/ConsolePanel.js` -> `components/ConsolePanel.mls` +- [x] `components/ToolbarPanel.js` -> `components/ToolbarPanel.mls` +- [x] `components/TreeNode.js` -> `components/TreeNode.mls` +- [x] `components/FileExplorer.js` -> `components/FileExplorer.mls` +- [x] `components/ReservedPanel.js` -> `components/ReservedPanel.mls` +- [x] `editor/editor.js` -> `editor/editor.mls` +- [x] `components/EditorPanel.js` -> `components/EditorPanel.mls` +- [x] `execution/worker.js` -> `execution/worker.mls` +- [x] `main.js` -> `main.mls` + +## Current Step + +Done: all tracked hand-written Web IDE JavaScript files have been rewritten, and final full verification passed. + +## Verification + +- `timeout 300s sbt "hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide"` passed after `PanelPersistence.mls`. +- Browser smoke from `http://127.0.0.1:8125/index.html?panel-persistence=20260517` loaded `PanelPersistence.mjs`, restored/saved file explorer, diagnostics, and console panel sizes, compiled `main.mls`, and executed the default program. +- `timeout 300s sbt "hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide"` passed after `ResizeHandle.mls`. +- Browser smoke from `http://127.0.0.1:8126/index.html?resize-handle=20260517b` loaded `ResizeHandle.mjs`, did not load `ResizeHandle.js`, resized file explorer and console panels, persisted their sizes, compiled `main.mls`, and executed the default program. +- `timeout 300s sbt "hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide"` passed after `ConsolePanel.mls`. +- Headless browser smoke from `http://127.0.0.1:8127/index.html?console-panel=20260517d` loaded `ConsolePanel.mjs`, did not load `ConsolePanel.js`, rendered logs, collapsed/restored the panel, preserved logs, compiled `main.mls`, and executed the default program. +- `timeout 300s sbt "hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide"` passed after `ToolbarPanel.mls`. +- Headless browser smoke from `http://127.0.0.1:8128/index.html?toolbar=20260517b` loaded `ToolbarPanel.mjs`, did not load `ToolbarPanel.js`, dispatched compile for `/main.mls`, updated status/button states, showed and hid the status tooltip, and executed the default program. +- `timeout 300s sbt "hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide"` passed after `TreeNode.mls`. +- Headless browser smoke from `http://127.0.0.1:8129/index.html?treenode=20260517b` loaded `TreeNode.mjs`, did not load `TreeNode.js`, opened source and compiled-output files from the tree, hid paired `.mjs` entries, updated folder children, removed stale `.mjs` buttons, and executed the default program. +- `timeout 300s sbt "hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide"` passed after `FileExplorer.mls`. +- Headless browser smoke from `http://127.0.0.1:8130/index.html?fileexplorer=20260517a` loaded `FileExplorer.mjs`, did not load `FileExplorer.js`, toggled sidebar collapse, created and opened a new file through the UI, showed and hid the tree tooltip, compiled `main.mls`, and executed the default program. +- `timeout 300s sbt "hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide"` passed after `ReservedPanel.mls`. +- Headless browser smoke from `http://127.0.0.1:8131/index.html?reserved-panel=1778962322` loaded `ReservedPanel.mjs`, did not load `ReservedPanel.js`, compiled and executed `main.mls`, rendered diagnostics, and exercised goto, diagnostic toggle, file toggle, collapse-all, and panel collapse behavior. +- `timeout 300s sbt "hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide"` passed after `editor/editor.mls`. +- Headless browser smoke from `http://127.0.0.1:8131/index.html?editor=1778962677` loaded `editor.mjs`, did not load `editor.js`, created mutable CodeMirror editor views, autosaved edits, compiled and executed edited `main.mls`, and kept readonly std-file edits from persisting. +- `timeout 300s sbt "hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide"` passed after `EditorPanel.mls`. +- Headless browser smoke from `http://127.0.0.1:8131/index.html?editor-panel=1778963227` loaded `EditorPanel.mjs`, did not load `EditorPanel.js`, kept public tab state, opened files, synchronized write/rename/delete filesystem events, navigated to a line, handled keyboard compile/execute, disabled std tabs, and closed a tab with Ctrl-W. +- `timeout 300s sbt "hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide"` passed after `execution/worker.mls`. +- Headless browser smoke from `http://127.0.0.1:8131/index.html?worker2=1778964086` loaded `execution/worker.mjs`, did not load `execution/worker.js`, compiled and executed `main.mls`, loaded SES/Endo worker dependencies, resolved VM module imports, and forwarded execution console output with no browser console errors. +- `timeout 300s sbt "hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide"` passed after `main.mls`. +- Headless browser smoke from `http://127.0.0.1:8131/index.html?main=1778964426` loaded `main.mjs`, did not load `main.js`, cleared persisted files, bootstrapped the default `main.mls`, compiled and executed it, and reported no browser console errors. +- `timeout 1800s sbt hkmc2AllTests/test` passed after all hand-written Web IDE JavaScript files were rewritten. diff --git a/docs/web-ide-mlscript-rewrite-workflow.md b/docs/web-ide-mlscript-rewrite-workflow.md new file mode 100644 index 0000000000..7b99d91e18 --- /dev/null +++ b/docs/web-ide-mlscript-rewrite-workflow.md @@ -0,0 +1,87 @@ +# Web IDE MLscript Rewrite Workflow + +This workflow is for LLM agents rewriting the remaining Web IDE JavaScript +modules under `hkmc2/shared/src/test/mlscript-packages/web-ide` into MLscript. + +## Principles + +- Rewrite one module boundary at a time. +- Preserve the JavaScript module contract first: exported names, default export + shape, callbacks, events, error behavior, and import paths. +- Treat the existing JavaScript file as the behavior spec. +- Keep commits scoped to one conceptual change. +- Do not reformat unrelated whitespace or blank lines. + +## Steps + +1. Pick a small boundary. + Prefer leaf modules or modules with already-ported callers. Read the target + JavaScript file and every direct caller before writing MLscript. + +2. Port the implementation. + Add a `.mls` file next to the old `.js` file. Follow existing Web IDE + MLscript style: UCS conditionals, conjunctive `and` guards, direct declared + globals when possible, and shared helpers such as `common/JS.mls`. + +3. Keep browser APIs explicit. + If the port needs a browser or JavaScript global, add the smallest useful + declaration to `hkmc2/shared/src/test/mlscript/decls/Prelude.mls`. Use + `globalThis` only when MLscript/codegen cannot express the access, and leave + a short comment explaining why. + +4. Switch imports deliberately. + After package compilation generates the `.mjs`, update callers from the old + `.js` module to the generated `.mjs`. Delete the old `.js` file only after + the generated module is actually used. + +5. Run package verification. + Run: + + ```sh + timeout 300s sbt "hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide" + ``` + + Check `git status` afterward. Generated `.mjs` files and generated + `vendors/` output should stay untracked. + +6. Run a browser smoke test. + Start a static server from the Web IDE package directory: + + ```sh + cd hkmc2/shared/src/test/mlscript-packages/web-ide + python3 -m http.server 8123 --bind 127.0.0.1 + ``` + + Open `http://127.0.0.1:8123/index.html?`, then verify: + + - initial page load has no browser console errors; + - default `main.mls` is visible; + - Compile works and produces the normal compiler output; + - Execute works and the worker runs; + - the browser console still has no errors afterward. + + Also test one behavior owned by the rewritten module, for example: + + - filesystem: create, rename, delete, read, and list files; + - persistence: create a file, reload, and confirm it survives; + - runner: compile first, then execute through the rewritten runner; + - compiler: compile through the rewritten compiler client or worker; + - UI component: perform the hover, click, input, or focus action it owns. + +7. Run broader verification when needed. + Run `timeout 1500s sbt hkmc2AllTests/test` before committing if the change + touches shared compiler behavior, `Prelude.mls`, module resolution, codegen, + package vendoring, or golden-test-visible behavior. + +8. Commit only reviewed output. + Review `git diff`, run `git diff --check`, and keep any golden snapshot + rewrites only when they are intentional. Use a one-line commit message. + +## Style Notes + +- Do not write `else if`; use Ultimate Conditional Syntax. +- Prefer `if value is ~Absent and ...` over nested null checks. +- Do not recreate local `isAbsent` helpers; import the shared `Absent` pattern. +- Prefer direct declarations from `Prelude.mls` over `globalThis`. +- Do not remove existing `end` markers. +- Do not use `asInstanceOf` unless there is no reasonable typed alternative. diff --git a/docs/web-ide-mlscript-second-pass-workflow.md b/docs/web-ide-mlscript-second-pass-workflow.md new file mode 100644 index 0000000000..e753782b2e --- /dev/null +++ b/docs/web-ide-mlscript-second-pass-workflow.md @@ -0,0 +1,82 @@ +# Web IDE MLscript Second-Pass Workflow + +This workflow is for improving the already-rewritten Web IDE MLscript files +after the first JavaScript-to-MLscript port. The goal is idiomatic MLscript, +not another behavior rewrite. + +## Inputs + +- Use `docs/idomatic-mlscript-rules.md` as the rule list. +- Treat the current Web IDE behavior as fixed unless a bug is explicitly in + scope. +- Preserve existing whitespace style, including blank-line indentation. + +## Rewrite Loop + +1. Pick one idiom rule. + Prefer one checklist item from `docs/idomatic-mlscript-rules.md`. + +2. Apply it to one small file first. + This keeps syntax, type, and style problems easy to inspect. + +3. Compile the package. + Run: + + ```sh + timeout 300s sbt "hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide" + ``` + +4. Expand the same rule to other relevant files. + Keep the pass scoped to that rule unless a nearby small cleanup is required + for the same rewrite. + +5. Check the diff. + Run: + + ```sh + git diff --check + git status --short + ``` + + No golden snapshots or unrelated test files should change. + +6. Run browser verification. + Prefer a headless browser so the user's desktop is not disturbed. Start a + local server from the Web IDE package directory: + + ```sh + cd hkmc2/shared/src/test/mlscript-packages/web-ide + python3 -m http.server 8123 --bind 127.0.0.1 + ``` + + Open `http://127.0.0.1:8123/index.html?` and verify: + + - the page loads without browser console errors; + - files are visible in the explorer; + - a source file opens in the editor; + - Compile still works; + - Execute still works; + - the specific UI or runtime behavior touched by the rewrite still works. + +7. Run broader tests when needed. + If the pass touches shared compiler behavior, `Prelude.mls`, package + vendoring, module resolution, generated JavaScript shape, or golden-test + output, run: + + ```sh + timeout 1800s sbt hkmc2AllTests/test + ``` + +8. Update the checklist. + Mark completed rules in `docs/idomatic-mlscript-rules.md`. + +9. Commit the pass. + Use a short one-line commit message with no body. Prefer one commit per + idiom rule; use one commit per file only when the change is risky. + +## Commit Shape + +- Cross-file mechanical cleanup: one rule, one commit. +- Single-file polish: one coherent group, one commit. +- Risky behavior change: isolate it in its own commit. + diff --git a/docs/web-ide-polish-plan.md b/docs/web-ide-polish-plan.md new file mode 100644 index 0000000000..f2f692dd48 --- /dev/null +++ b/docs/web-ide-polish-plan.md @@ -0,0 +1,794 @@ +# Web IDE Polish Implementation Plan + +This document consolidates the remaining Web IDE polish work into a formal implementation plan. It is intended to be executed as a sequence of small, reviewable commits. The plan is split into two workstreams: + +- Part A covers general polish, editor quality-of-life improvements, persistence, accessibility, documentation, and small correctness checks. +- Part B introduces a Settings dialog and then wires existing or new preferences through a shared settings store. + +The plan intentionally favors narrow changes over broad refactors. Each task should be checked against the current implementation before any edits are made, because some items may already have been completed by earlier sessions. + +## Execution Rules + +1. Use one commit per task unless a task is explicitly described as a grouped check. +2. Touch as few files as possible for each commit. +3. Edit `.mls` source files, not generated `.mjs` files. +4. Preserve existing indentation, blank lines, and `end` markers. +5. Do not perform unrelated refactors, renames, formatting passes, or cleanup. +6. When exact text, keys, code snippets, or URLs are specified here, use them verbatim. +7. Do not introduce new mock data except where this plan explicitly requests it. +8. Keep Source Control, Terminal, and generated-output panels hidden unless a later task explicitly enables them. +9. After `SettingsStore` exists, all new user preferences must go through it. Do not add new ad hoc `localStorage` calls after that point. +10. Work on a branch or worktree and do not push unless explicitly asked. +11. Update `PLAN.md` only to describe work that actually shipped. + +## Validation Rules + +After each implementation task, run: + +```sh +sbt --client "hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide" +``` + +If the task is documentation-only, a package test is optional, but `git diff --check` should still be run before committing. + +Before the final closing commit for this plan, run: + +```sh +sbt --client "hkmc2PackagesTest/test" +``` + +## Skip Policy + +Before implementing a task, inspect the relevant files and behavior. If the code already satisfies the task, skip the task and mention the skip in the commit summary or progress note. Do not rework working code for style preference alone. + +Some tasks are explicitly investigative. For those tasks, only make a code change if there is a clear and local defect. If no defect is found, record that the check was completed and move on. + +--- + +# Part A: General Polish and Quality-of-Life + +Part A can be executed independently of most Settings work. The only planned dependency is that the editor font-size implementation in A2 should land before the Settings row that controls it in Part B. + +## A1. Known Issues Review + +### A1.1 Review CodeMirror folding logic + +Inspect `foldRange` and `mlscriptFolding` in `editor/editor.mls`. The goal is not to redesign folding. Only fix an obvious local bug, such as: + +- a fold range that starts or ends one line too early or too late, +- a missing null or absent-value check, +- a range that includes invalid line numbers, +- a clearly inverted condition. + +If the implementation is coherent and no local defect is visible, skip this task. + +### A1.2 Review fold gutter styling + +Inspect `.cm-foldGutter` and related fold-marker CSS in `style.css`. If the app relies on CodeMirror's default fold gutter markers with no partial custom styling, skip this task. Only make a change if there is a half-finished override that visually conflicts with the rest of the editor or breaks interaction. + +## A2. Editor Projection Legibility + +This work creates the editor font-size control that the Settings dialog will later expose. Implement the editor behavior first, then wire the Settings row in Part B. + +### A2.1 Add an editor font-size CSS variable + +Add `--editor-font-size` to `:root` in `style.css`. Use this variable only for CodeMirror editor text, specifically `.cm-editor` and `.cm-content` font sizing. Do not apply it to sidebars, tabs, dialogs, buttons, or status text. + +Expected result: editor text size can be changed by setting a single CSS custom property. + +### A2.2 Add editor font-size adjustment logic + +Add `bumpEditorFontSize(delta)` in `editor/editor.mls`. It should: + +- read the current editor font size, +- add `delta`, +- clamp the result to the range 10-24 pixels, +- write the result back through `--editor-font-size`. + +Use the app's existing style and helper conventions. Avoid duplicating persistence logic here if that logic already lives elsewhere. + +### A2.3 Add keyboard shortcuts for font-size changes + +Wire Cmd/Ctrl+= and Cmd/Ctrl+- into the existing `handleShortcut` chain in `EditorPanel.mls`. + +Do not bind: + +- Cmd/Ctrl+0, because browsers reserve it for page zoom reset, +- Cmd/Ctrl+B, because it is already used for the left-sidebar toggle. + +Expected result: the active editor font size increases or decreases without affecting browser zoom. + +### A2.4 Persist editor font size + +Persist the editor font size using `PanelPersistence.mls` and its existing localStorage convention. Do not introduce a separate storage pattern for this task. + +Expected result: after reload, the editor uses the last selected font size. + +## A3. Examples Panel + +The Examples panel should be made functional with two small built-in examples. The content is intentionally minimal; better examples can be supplied later. + +### A3.1 Add initial example data + +Add exactly the following two examples and no additional entries: + +```text +{id: "pattern-matching", title: "Pattern Matching", code: "data class Cons(head, tail)\ndata object Nil\n\nfun sum(list) = if list is\n Cons(h, t) then h + sum(t)\n Nil then 0\n\nsum(Cons(1, Cons(2, Cons(3, Nil))))"} +``` + +```text +{id: "recursion", title: "Recursion", code: "fun factorial(n) =\n if n <= 1 then 1\n else n * factorial(n - 1)\n\nfactorial(5)"} +``` + +If either example causes the package test to fail, replace the failing example body with `1 + 1` rather than spending time debugging example syntax. + +### A3.2 Replace mock example source + +Update `ExamplesPanel` in `components/MockActivityPanels.mls` to use the new example data instead of `mockWorkbenchData.examples`. + +Remove the "Mock library" label. Keep the rail button hidden for now. + +### A3.3 Implement load-example behavior + +Wire "load example" so it reuses the same create-and-open file flow already used by `FileExplorer.mls` or `main.mls`. + +Expected result: selecting an example creates or opens a file containing the example code, then focuses it in the editor. + +### A3.4 Unhide the Examples rail button + +Only perform this task if A3.1-A3.3 passed. Unhide the Examples rail button in `IdeWorkbench.mls`. + +If any earlier Examples task was skipped or failed, leave the rail button hidden and explain why in the commit note. + +### A3.5 Update visible-plan documentation + +Update `PLAN.md` to reflect only the Examples work that actually shipped. + +## A4. Status Bar and Editor Info + +The status bar should expose basic editor state without requiring new layout surfaces. + +### A4.1 Dispatch cursor-position updates + +In `editor/editor.mls`, dispatch a `cursor-position-changed` DOM event whenever the CodeMirror selection changes. The event detail should include: + +- line number, +- column number. + +Use 1-based line and column values for user-facing display. + +### A4.2 Render cursor position in the existing status bar + +Search for an existing status-bar component or element first, using `status-bar` across `components/*.mls`. If a status bar already exists, reuse it. Do not create a second status bar. + +Render the current cursor position as: + +```text +Ln X, Col Y +``` + +### A4.3 Render selected-character count + +Using the same listener, render: + +```text +(N selected) +``` + +only when the current selection length is greater than zero. Hide this label when there is no selection. + +### A4.4 Add encoding and line-ending labels + +Add a static status-bar label: + +```text +UTF-8 LF +``` + +This is a hardcoded product assumption, not computed from file contents. + +### A4.5 Add indentation label + +Add a static status-bar label: + +```text +Spaces: 2 +``` + +This is also hardcoded. Do not infer it from the active file. + +## A5. Layout and Filter Persistence + +These persistence keys will also be used by the Settings "Reset Layout to Default" action. Keep key names stable once chosen. + +### A5.1 Persist left sidebar width + +Persist the user-adjusted width of the left sidebar. Restore it during workbench initialization. + +Expected result: resizing the left sidebar survives page reload. + +### A5.2 Persist right sidebar width + +Persist the user-adjusted width of the right sidebar. Restore it during workbench initialization. + +Expected result: resizing the Problems sidebar survives page reload. + +### A5.3 Persist bottom panel height + +Persist the user-adjusted bottom panel height. Restore it during workbench initialization. + +Expected result: resizing the Output/Logging panel survives page reload. + +### A5.4 Persist last active bottom tab + +Persist whether the bottom panel last showed Output or Logging. Restore that tab on reload. + +This should not force the bottom panel open if the panel is intentionally collapsed, unless current behavior already does so. + +### A5.5 Persist Problems panel scope + +Persist the Problems panel scope, either Workspace or Current file. Restore it when the diagnostics inspector initializes. + +### A5.6 Persist Logs filters + +Persist the Logs level filter and source filter. Restore both values when the bottom panel initializes. + +If source options are generated dynamically from log entries, restore the saved source only when it exists in the current option set. + +## A6. Discoverability + +These tasks make existing actions easier to find through command palette entries, shortcuts, or consistent dialog behavior. + +### A6.1 Add New File to the command palette + +Add a command palette item labeled "New File". Do not add a keyboard shortcut, because Cmd/Ctrl+N is browser-reserved. + +The command should reuse the existing new-file flow rather than duplicating file-creation logic. + +### A6.2 Add Problems panel toggle shortcut + +Add a right-rail Problems toggle bound to Cmd/Ctrl+Shift+B. This shortcut is confirmed not to collide with the existing Cmd/Ctrl+B left-sidebar toggle. + +Expected result: the shortcut opens or closes the Problems panel. + +### A6.3 Add Clear Output action if missing + +Check whether a Clear Output button and command already exist. If either is missing, add the missing surface and reuse the existing output-clear behavior. + +Do not create a second output-clearing implementation. + +### A6.4 Add Clear Logs action if missing + +Check whether a Clear Logs button and command already exist. If either is missing, add the missing surface and reuse the existing logs-clear behavior. + +Do not create a second logs-clearing implementation. + +### A6.5 Add Export Project to the command palette + +Add an "Export Project" command palette item that invokes the existing project export behavior. + +Expected result: users can export the current project without opening the Project Switcher first. + +### A6.6 Audit Escape handling for dialogs + +Check exactly these dialogs: + +- Command Palette, +- Share dialog, +- Project Switcher. + +Each should close on Escape. Fix only a missing or broken Escape case. Do not refactor dialog infrastructure. + +## A7. Small Polish + +### A7.1 Add middle-click tab close + +In `EditorPanel.mls`, add support for closing editor tabs with a middle-click, where `event.button === 1`. + +Prevent browser autoscroll behavior if necessary. + +### A7.2 Verify dirty-dot clearing after undo + +In `EditorPanel.mls`, check whether undoing back to the saved state clears the dirty indicator on the tab. + +Only fix this exact case if it is broken. Do not redesign dirty-state tracking. + +### A7.3 Add rail button aria labels + +Add missing `aria-label` attributes to rail icon buttons in `IdeWorkbench.mls` only. + +Use concise labels that match the visible panel purpose, such as "Files", "Search", "Outline", or "Problems". + +### A7.4 Add temporary Copy Markdown feedback + +For the Problems "Copy Markdown" action, briefly relabel the button itself to: + +```text +Copied! +``` + +Then revert after approximately one second. Do not add a new toast, snackbar, or notification element. + +### A7.5 Review dialog and tooltip z-index usage + +Check z-index usage in exactly: + +- `NativeDialogs.mls`, +- `ProjectSwitcher.mls`. + +Compare dialog layering against `--z-tooltip`. Fix only a clear inconsistency where a tooltip or dialog can incorrectly cover the other. + +### A7.6 Check Output auto-scroll during execution + +Check whether Output auto-scrolls on every new execution chunk, not only once when execution starts. + +If auto-scroll only happens at the beginning, update the append path so each new chunk keeps the latest output visible when the user has not intentionally scrolled away. + +## A8. Accessibility + +### A8.1 Audit icon-only toolbar and rail buttons + +Inspect exactly: + +- `IdeWorkbench.mls`, +- `ToolbarPanel.mls`. + +Add missing `aria-label` attributes to icon-only buttons. Do not broaden this task into a full accessibility rewrite. + +### A8.2 Audit removed focus outlines + +Search `style.css` for `outline: none` on focusable elements. If a focus outline was removed without a replacement focus style, add a visible replacement. + +Expected result: keyboard users can see focus location on buttons, tabs, inputs, and dialog controls. + +### A8.3 Verify semantic color contrast + +Compute relative-luminance contrast ratios for these text colors against `--ide-bg`: + +- `--ide-danger`, +- `--ide-warning`, +- `--ide-info`. + +Use the WCAG AA threshold of 4.5:1 for normal text. Only change a color if the computed ratio is clearly below threshold. + +## A9. Documentation Pass + +Run this after Part A and Part B implementation work so the docs describe reality rather than planned behavior. + +### A9.1 Update `PLAN.md` + +Confirm `PLAN.md` matches the behavior currently visible in the Web IDE. Remove claims about features that did not ship. + +### A9.2 Update `AGENTS.md` + +Confirm `AGENTS.md` reflects the current component layout, important event names, and development workflow for the Web IDE package. + +### A9.3 Update package `README.md` + +Confirm the package README matches the current setup, build, and run steps. + +## A10. Stretch Filler + +These are low-priority tasks for when the main polish work is complete. Keep them small. + +### A10.1 Route stray console logging through Logger + +Search for `console.log` calls that are not routed through `common/Logger.mls`. + +Fix up to five instances, then stop. Do not attempt a full logging migration in this task. + +### A10.2 Proofread visible copy + +Proofread toolbar labels, dialog copy, and empty-state text for clear misspellings only. + +Do not rewrite tone, terminology, or product language unless there is an obvious typo. + +### A10.3 Add a favicon only if absent + +Check `index.html` for a custom favicon ``. If one is entirely absent, add a minimal favicon. + +Do not replace an existing favicon. + +### A10.4 Cross-check shortcut tooltips + +Compare shortcut tooltips against actual bindings in `handleShortcut`. + +Fix label text only when it is mismatched. Do not add new shortcuts in this task. + +### A10.5 Read ZIP export path for obvious bugs + +Read the ZIP export code path once and look for an obvious local defect, such as a missing file, wrong path prefix, or unhandled empty project. + +Do not perform a live export test as part of this task unless explicitly requested. + +### A10.6 Run final package test and closing summary + +Run: + +```sh +sbt --client "hkmc2PackagesTest/test" +``` + +Then make a closing summary commit that updates the relevant plan documentation to match the actual completed work. + +--- + +# Part B: Settings Panel + +Part B creates a Settings dialog and then wires preferences through a shared settings store. Phase 0 is mandatory foundation work. Do not start Phase 1, 2, or 3 until Phase 0 is complete. + +## Phase 0: Settings Foundation + +### B0.1 Add `SettingsStore` + +Create `common/SettingsStore.mls`. + +Expose: + +```text +getSetting(key, default) +setSetting(key, value) +``` + +Back the store with `localStorage`. Prefix every key with: + +```text +settings. +``` + +Follow the style of `PanelPersistence.mls`: plain functions, no JSON blob that stores all preferences together. + +### B0.2 Create the base Settings dialog component + +Create `components/SettingsDialog.mls`. + +Use `ShareDialog` in `components/NativeDialogs.mls` as the structural model. The new component should use: + +- `connectedCallback`, +- `render()`, +- `attachEventListeners()`, +- `dialog()`, +- `openDialog()`. + +The rendered HTML should contain a native dialog: + +```html + +
+ ... +
+
+``` + +The header should contain the title "Settings" and a close button. The body can be empty until the tab task lands. + +### B0.3 Wire Settings dialog dismissal and open event + +Inside `SettingsDialog.attachEventListeners()`: + +- use the existing `closeDismissibleDialogFromBackdrop(dialog())`, +- register a listener for `settings-dialog-open-requested`, +- call `openDialog()` when that event is received. + +Register the custom element: + +```text +customElements.define("settings-dialog", SettingsDialog) +``` + +Do not duplicate the backdrop-dismiss helper. + +### B0.4 Mount Settings dialog in the workbench + +Add: + +```html + +``` + +in `components/IdeWorkbench.mls` next to the existing ``. + +### B0.5 Add Settings toolbar button + +Add a `#settings` icon button to `ToolbarPanel.mls` inside the `.actions` area. + +Copy the existing `#share` button pattern: + +- add a `handleSettings()` method, +- dispatch `settings-dialog-open-requested`, +- wire the handler in `attachButtonListeners`. + +### B0.6 Add Settings tabs + +Add tab switching inside the Settings dialog body. Create seven tab buttons with `data-settings-tab`: + +- Appearance, +- Editor, +- Workbench, +- Compile & Run, +- Keyboard Shortcuts, +- Data, +- About. + +Create seven matching content panels with `data-settings-tab-panel`. + +Add `switchTab(id)` to toggle `.active` on both the selected tab and panel. Mirror the existing rail-panel active-class convention documented in `AGENTS.md`. Default to Appearance. + +### B0.7 Add base Settings styles + +Add CSS for: + +- `.settings-dialog-native`, +- `.settings-tabs`, +- `.settings-tab-content`. + +Use existing `--ide-*` tokens and the sizing approach used by `.share-dialog-native` and `.project-switcher-native`. Do not introduce a new visual language. + +### B0.8 Add canonical Settings row templates + +Add one canonical row shape for each row kind. + +Toggle row: + +```html + +``` + +Select row: + +```html + +``` + +Button row: + +```html +
+ {label} + +
+``` + +Static row: + +```html +
+ {label} + {value} +
+``` + +Add one delegated change listener. Changes from `.settings-toggle` and `.settings-select` should call `SettingsStore.setSetting(el.dataset.settingKey, value)`. + +### B0.9 Load persisted Settings values into the dialog + +Add `loadSettingsIntoDialog()`. + +Call it from `openDialog()`. It should: + +- find all elements with `data-setting-key`, +- read their stored values with `SettingsStore.getSetting(...)`, +- apply checked state for toggles, +- apply selected value for selects. + +This must be generic and reused by all later settings rows. + +## Phase 1: Editor and Workbench Settings + +Each Phase 1 task should be small because Phase 0 supplies the store, dialog, tabs, and row templates. + +### B1.1 Add Editor font-size setting + +Add an Editor tab select row using key: + +```text +editor.fontSize +``` + +Reuse the `--editor-font-size` variable from A2. Update `editor.mls` so editor creation applies the stored value. + +Expected result: the setting changes editor font size and persists across reload. + +### B1.2 Add Editor word-wrap setting + +Add an Editor tab toggle using key: + +```text +editor.wordWrap +``` + +When enabled, include CodeMirror's `EditorView.lineWrapping` extension during editor creation. + +Expected result: long lines wrap only when the setting is enabled. + +### B1.3 Add Editor indentation-guides setting + +Add an Editor tab toggle using key: + +```text +editor.indentGuides +``` + +When enabled, include the existing indentation-guide extension. When disabled, omit it. + +Do not reimplement indentation guides in this task. + +### B1.4 Add Editor show-whitespace setting + +Add an Editor tab toggle using key: + +```text +editor.showWhitespace +``` + +When enabled, include `highlightWhitespace()` from `@codemirror/view`. + +Expected result: whitespace markers appear only when the setting is enabled. + +### B1.5 Add Reset Layout to Default action + +Add a Workbench tab button labeled: + +```text +Reset Layout to Default +``` + +The action should clear exactly the layout and filter keys introduced in A5: + +- left sidebar width, +- right sidebar width, +- bottom panel height, +- last active bottom tab, +- Problems panel scope, +- Logs level filter, +- Logs source filter. + +Reload the app after clearing those keys. Do not clear project or file contents. + +### B1.6 Add startup Problems panel setting + +Add a Workbench tab toggle using key: + +```text +workbench.showProblemsOnStartup +``` + +Wire it where the right rail's initial open/closed state is set in `IdeWorkbench.mls`. + +Expected result: users can choose whether the Problems panel opens on startup. + +### B1.7 Add reopen-last-project setting + +Add a Workbench tab toggle using key: + +```text +workbench.reopenLastProject +``` + +Inspect `filesystem/projects.mls` to find where the initial project is selected. Make that behavior conditional on the setting. + +Expected result: users can disable automatic reopening of the previous project. + +## Phase 2: Compile and Run, Keyboard Shortcuts, and About + +### B2.1 Add default Problems scope setting + +Add a Compile & Run tab select row using key: + +```text +problems.defaultScope +``` + +Options: + +- Workspace, +- Current file. + +Update `DiagnosticsInspector.mls` so the selected value is used as the initial scope. + +### B2.2 Add default Logs level setting + +Add a Compile & Run tab select row using key: + +```text +logs.defaultLevel +``` + +Update `BottomPanel.mls` so it uses this setting as the initial `logLevelFilter` instead of hardcoding `"all"`. + +Do not add a default source row. Log sources are built dynamically from log entries, so there is no stable option set at startup. + +### B2.3 Add Keyboard Shortcuts static list + +Add a static list in the Keyboard Shortcuts tab. + +Build the list from the shortcuts actually bound in `EditorPanel.mls`'s `handleShortcut` at the time this task is implemented. Do not copy a stale shortcut list from this document. + +Expected result: the Settings dialog documents the shortcuts that actually work. + +### B2.4 Add About version label + +Add a static version label to the About tab. + +Use a simple hardcoded string. `manifest.json` has no version field, so do not invent a new version source. + +### B2.5 Add About project link + +Add a static link to: + +```text +https://github.com/hkust-taco/mlscript +``` + +This URL is confirmed from the repository remote. Do not substitute another project URL. + +## Phase 3: Larger Settings Work + +These tasks touch broader behavior or visual design. Do them last and review each one more carefully than the earlier Settings tasks. + +### B3.1 Add editor-only dark theme setting + +Add an Appearance tab toggle for the editor theme only. + +When enabled, swap `vscodeLight` to `vscodeDark` from the same `@uiw/codemirror-theme-vscode` package. + +This is explicitly not full workbench dark mode. Do not change workbench colors in this task. + +### B3.2 Add auto-compile-on-save setting + +Add a Compile & Run tab toggle for auto-compile-on-save. + +Wire it into `editor.mls`'s `saveOnChange` flow so saving conditionally dispatches `compile-requested`. + +This changes program flow, so verify that manual compile still works and that disabled auto-compile does not dispatch compile requests. + +### B3.3 Add auto-run-after-compile setting + +Add a Compile & Run tab toggle for auto-run-after-compile. + +Wire it wherever `compilation-status-change` emits `"done"` in `compiler/index.mls`. When enabled, successful compile completion should dispatch `execute-requested`. + +This changes program flow, so verify that manual execute still works and that failed compilation does not auto-run. + +### B3.4 Add Reset app to defaults action + +Add a Data tab button labeled: + +```text +Reset app to defaults +``` + +Do not implement this as "clear all localStorage." Hardcode the exact list of keys to clear: + +- all `settings.*` keys, +- the panel-size and filter keys from A5. + +Explicitly exclude any key that stores project contents, file contents, workspace contents, or user-created data. + +Require a native `confirm()` before clearing. If the exact key list cannot be confirmed with confidence, skip this task rather than guess. + +### B3.5 Reserve full workbench dark mode + +Leave the Appearance tab section for full workbench dark mode reserved but empty. + +Do not attempt this in an unattended batch. Full workbench dark mode needs actual design input for colors, contrast, and component states. + +--- + +# Recommended Sequencing + +1. Complete Part A tasks that are independent and low risk. +2. Complete A2 before B1.1, because B1.1 depends on the editor font-size variable and adjustment behavior. +3. Complete A5 before B1.5, because Reset Layout needs the exact persistence keys. +4. Complete all of Part B Phase 0 before any later Settings task. +5. Complete Phase 1 and Phase 2 Settings rows before Phase 3 behavior changes. +6. Run Part A9 documentation tasks after implementation work, not before. +7. Use A10 only as stretch work after the main plan is stable. + +This plan currently contains 72 tasks. If an implementation task is skipped because the code already satisfies it, keep the task number reserved and note the skip in the relevant progress summary. diff --git a/docs/web-ide-ui-framework-phases/phase-01-workbench-shell.md b/docs/web-ide-ui-framework-phases/phase-01-workbench-shell.md new file mode 100644 index 0000000000..48cc9df495 --- /dev/null +++ b/docs/web-ide-ui-framework-phases/phase-01-workbench-shell.md @@ -0,0 +1,56 @@ +# Phase 1: Workbench Shell Progress Tracker + +Parent plan: [MLscript Web IDE UI Framework Plan](../web-ide-ui-framework-plan.md) + +## Status + +- [ ] Not started +- [x] In progress +- [x] Browser verified +- [x] Tests passed +- [x] Committed + +## Goal + +Create the native custom-element workbench shell that owns the IDE layout. This phase establishes the structure for the titlebar, left activity rail, left panel host, editor region, right diagnostics inspector, bottom panel region, and status bar without trying to complete visual parity. + +## Deliverables + +- [x] Add a top-level `` custom element implemented in MLscript. +- [x] Move shell panel selection and layout state out of `main.mls` into the workbench shell. +- [x] Embed the existing `` as the real Files panel. +- [x] Embed the existing `` as the real editor region. +- [x] Replace the current right activity rail with a permanent diagnostics inspector region. +- [x] Route titlebar Compile and Execute controls through the existing `compile-requested` and `execute-requested` events. +- [x] Add status bar structure with only functional controls; render non-interactive status as plain text. + +## Visible Functionality + +- [x] Compile dispatches the current compile flow. +- [x] Execute dispatches the current execute flow and opens the output area if present. +- [x] Left rail buttons switch panels. +- [x] Clicking the active left rail item hides and reopens the left panel. +- [x] Diagnostics inspector hide/show control toggles the inspector. +- [x] No visible shell control is a dead button. + +## Mock Inventory Impact + +- Expected new mock entries: none. +- If any framework-only visible control is added, update the Mock Inventory in the parent plan before commit. + +## Verification + +- [x] Run `timeout 300s sbt "hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide"`. +- [x] Run `git diff --check`. +- [x] Run `git status --short` and confirm only intended files changed. +- [x] Start the local Web IDE server and open `http://127.0.0.1:8123/index.html?` with Playwright. +- [x] Verify the page loads without console errors. +- [x] Verify Files, editor open, Compile, Execute, diagnostics display, and panel scrolling still work. +- [x] Verify desktop and narrow viewport layouts do not overflow horizontally. +- [x] Capture a 1920x1080 Playwright screenshot. + +## Completion Notes + +- Commit: this phase commit. +- Browser notes: Playwright CLI verified clean console output, Files collapse/reopen, diagnostics hide/show, active-file status updates, `.mls` compile, disabled `.mjs` compile, Execute reopening the collapsed console, and no horizontal overflow at 390x844. Screenshot: `docs/web-ide-ui-framework-screenshots/phase-01/workbench-shell-1920x1080.png`. +- Test output: `hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide` passed 21 tests. diff --git a/docs/web-ide-ui-framework-phases/phase-02-visual-foundation.md b/docs/web-ide-ui-framework-phases/phase-02-visual-foundation.md new file mode 100644 index 0000000000..4aaa481a41 --- /dev/null +++ b/docs/web-ide-ui-framework-phases/phase-02-visual-foundation.md @@ -0,0 +1,54 @@ +# Phase 2: Visual Foundation Progress Tracker + +Parent plan: [MLscript Web IDE UI Framework Plan](../web-ide-ui-framework-plan.md) + +## Status + +- [ ] Not started +- [x] In progress +- [x] Browser verified +- [x] Tests passed +- [x] Committed + +## Goal + +Establish the visual system for the new IDE shell before adding more panel surfaces. This phase defines the reusable layout and theme tokens that make the UI dense, readable, and prototype-aligned. + +## Deliverables + +- [x] Add CSS tokens for warm light theme colors. +- [x] Add dark theme scaffold only if the theme toggle is functional in this phase. +- [x] Add layout tokens for rail width, side panel width, right inspector width, bottom panel height, and status bar height. +- [x] Add shared tokens for borders, radius, shadows, typography, and diagnostic severity colors. +- [x] Restyle existing shell, file explorer, editor chrome, diagnostics region, bottom region, and status bar to use the new tokens. +- [x] Ensure editor, left panel, right inspector, and bottom panel occupy real layout space and scroll independently. + +## Visible Functionality + +- [x] Theme toggle is shown only if it actually switches themes. +- [x] Active rail, tab, segmented-control, and selected states are visually distinct. +- [x] Text remains readable in all visible regions. +- [x] No control text overlaps or clips at desktop width. +- [x] No control text overlaps or clips at narrow viewport width. + +## Mock Inventory Impact + +- Expected new mock entries: none. +- If a theme button is rendered but dark theme is only partial, either complete it in this phase or document it in the Mock Inventory as a mocked/future UI surface. + +## Verification + +- [x] Run `timeout 300s sbt "hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide"`. +- [x] Run `git diff --check`. +- [x] Run `git status --short` and confirm only intended files changed. +- [x] Browser-check light theme contrast and layout. +- [x] Browser-check dark theme if the theme button is visible. +- [x] Browser-check independent scrolling for editor, left panel, right inspector, and bottom panel. +- [x] Browser-check desktop and narrow viewport layout stability. +- [x] Capture a 1920x1080 Playwright screenshot. + +## Completion Notes + +- Commit: this phase commit. +- Browser notes: Playwright CLI verified clean console output, no theme toggle, independent overflow containers, Files collapse/reopen, `.mls` compile, Execute reopening the collapsed console, and no horizontal overflow at 390x844. Screenshot: `docs/web-ide-ui-framework-screenshots/phase-02/visual-foundation-1920x1080.png`. +- Test output: `hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide` passed 21 tests. diff --git a/docs/web-ide-ui-framework-phases/phase-03-left-activity-panels.md b/docs/web-ide-ui-framework-phases/phase-03-left-activity-panels.md new file mode 100644 index 0000000000..90ee1fca90 --- /dev/null +++ b/docs/web-ide-ui-framework-phases/phase-03-left-activity-panels.md @@ -0,0 +1,66 @@ +# Phase 3: Left Activity Panels Progress Tracker + +Parent plan: [MLscript Web IDE UI Framework Plan](../web-ide-ui-framework-plan.md) + +## Status + +- [ ] Not started +- [x] In progress +- [x] Browser verified +- [x] Tests passed +- [x] Committed + +## Goal + +Build the left activity panel framework with functional tool surfaces. Files and Search are real workspace-backed panels. + +Source Control, Outline, and Examples remain UI-framework mocks that prove switching, selection, filtering, and scrolling behavior. + +## Deliverables + +- [x] Keep Files backed by the existing ``. +- [x] Add a Search panel custom element with workspace results. +- [x] Add a Source Control panel custom element with static changed-file data. +- [x] Add an Outline panel custom element with static symbol data. +- [x] Add an Examples panel custom element with static examples. +- [x] Add a small mock data module for these panels. +- [x] Ensure each panel can scroll when content overflows. +- [x] Update the Mock Inventory in the parent plan for every mocked surface added or changed. + +## Visible Functionality + +- [x] Left rail switches Files, Search, Source Control, Outline, and Examples. +- [x] Clicking the active rail item hides and reopens the left panel. +- [x] Search input filters visible workspace results. +- [x] Search clear button empties the query and restores results. +- [x] Source Control stage/unstage controls move mock files between sections and update counts. +- [x] Outline entries visibly navigate, scroll the editor, or dispatch a visible mocked navigation signal. +- [x] Examples selection updates selected state and detail/preview content. +- [x] No mock panel contains dead buttons. + +## Mock Inventory Impact + +- Expected mock entries: + - Source Control panel + - Outline panel + - Examples panel +- Search panel is real workspace text search. The parent Mock Inventory does not list it. +- Update the parent plan if any additional mock controls, badges, commands, or datasets are introduced. + +## Verification + +- [x] Run `timeout 300s sbt "hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide"`. +- [x] Run `git diff --check`. +- [x] Run `git status --short` and confirm only intended files changed. +- [x] Browser-check every left rail item. +- [x] Browser-check each panel's visible controls against the functionality standard. +- [x] Browser-check panel scroll behavior with long mock content. +- [x] Browser-check Files, editor open, Compile, Execute, and diagnostics still work. +- [x] Capture a 1920x1080 Playwright screenshot. + +## Completion Notes + +- Commit: this phase commit. +- Browser notes: Playwright CLI verified clean console output, rail switching across Files/Search/Source Control/Outline/Examples, active rail collapse/reopen, Search filter and clear, scrollable Search content, no narrow viewport horizontal overflow, and preserved file open/compile/execute/diagnostics flow. +- Additional browser notes: Source Control stage/unstage counts, Outline mock navigation feedback, Examples selection/detail updates, and scrollable Source Control mock content were verified. Screenshot: `docs/web-ide-ui-framework-screenshots/phase-03/left-activity-panels-1920x1080.png`. +- Test output: `hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide` passed 23 tests. diff --git a/docs/web-ide-ui-framework-phases/phase-04-diagnostics-inspector.md b/docs/web-ide-ui-framework-phases/phase-04-diagnostics-inspector.md new file mode 100644 index 0000000000..bcf9e64dc3 --- /dev/null +++ b/docs/web-ide-ui-framework-phases/phase-04-diagnostics-inspector.md @@ -0,0 +1,56 @@ +# Phase 4: Diagnostics Inspector Progress Tracker + +Parent plan: [MLscript Web IDE UI Framework Plan](../web-ide-ui-framework-plan.md) + +## Status + +- [ ] Not started +- [x] In progress +- [x] Browser verified +- [x] Tests passed +- [x] Committed + +## Goal + +Replace the old reserved panel with a prototype-style diagnostics inspector that supports list, tree, and source-card views while preserving the diagnostics entrypoint needed by the current compiler flow. + +## Deliverables + +- [x] Add a diagnostics-focused custom element, such as ``. +- [x] Preserve a `setDiagnostics(diagnosticsPerFile)` method. +- [x] Add severity counts in the inspector header. +- [x] Add List, Tree, and Source modes with native controls. +- [x] Render an empty state when no compiler diagnostics are available. +- [x] Render real diagnostics passed through `setDiagnostics` where available. +- [x] Add rich source-card layout for Source mode. +- [x] Remove diagnostics entries from the Mock Inventory after replacing them with real diagnostics and an empty state. + +## Visible Functionality + +- [x] List mode shows diagnostic rows and selected mode state. +- [x] Tree mode groups diagnostics by severity and selected mode state. +- [x] Source mode shows cards with source excerpts and selected mode state. +- [x] Severity counts match the visible diagnostics dataset. +- [x] Clicking a diagnostic backed by a real file dispatches `open-file-at-location`. +- [x] Hide diagnostics collapses the inspector and a visible control reopens it. + +## Mock Inventory Impact + +- Diagnostics sample data and quick actions were removed from the UI and from the parent Mock Inventory. +- The inspector now starts with an empty state and renders real compiler/runtime diagnostics when they are provided. + +## Verification + +- [x] Run `timeout 300s sbt "hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide"`. +- [x] Run `git diff --check`. +- [x] Run `git status --short` and confirm only intended files changed. +- [x] Browser-check List, Tree, and Source mode switching. +- [x] Browser-check severity counts against visible items. +- [x] Browser-check diagnostic click-to-open behavior for real diagnostics. +- [x] Browser-check Compile still updates diagnostics. + +## Completion Notes + +- Commit: this phase commit. +- Browser notes: Playwright CLI verified the inspector exists, starts with the empty state, List/Tree/Source modes switch selected state, Source mode renders real diagnostic cards after `setDiagnostics`, Open dispatches the real file location, and Compile swaps the inspector to compiler diagnostics or the empty state. Screenshot: `docs/web-ide-ui-framework-screenshots/phase-04/diagnostics-inspector-1920x1080.png`. +- Test output: `hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide` passed 24 tests. diff --git a/docs/web-ide-ui-framework-phases/phase-05-bottom-panel.md b/docs/web-ide-ui-framework-phases/phase-05-bottom-panel.md new file mode 100644 index 0000000000..c3506403c7 --- /dev/null +++ b/docs/web-ide-ui-framework-phases/phase-05-bottom-panel.md @@ -0,0 +1,62 @@ +# Phase 5: Bottom Panel Progress Tracker + +Parent plan: [MLscript Web IDE UI Framework Plan](../web-ide-ui-framework-plan.md) + +## Status + +- [ ] Not started +- [x] In progress +- [x] Browser verified +- [x] Tests passed +- [x] Committed + +## Goal + +Replace the single console surface with a native tabbed bottom panel that separates Output, Problems, and Terminal while preserving the current runtime output flow. + +## Deliverables + +- [x] Add a `` custom element. +- [x] Add Output, Problems, and Terminal tabs with native tab state. +- [x] Route current console/runtime output into Output. +- [x] Add mocked Problems content. +- [x] Add mocked Terminal content. +- [x] Add Preserve logs, Clear, Download, and Hide controls. +- [x] Ensure Execute opens the Output tab if the bottom panel is hidden. +- [x] Update the Mock Inventory for Problems and Terminal. + +## Visible Functionality + +- [x] Output tab shows current output messages. +- [x] Problems tab shows mock problem rows. +- [x] Terminal tab shows mock terminal transcript or mutable mock lines. +- [x] Tab switching updates visible content and ARIA selected state. +- [x] Preserve logs changes Clear behavior. +- [x] Clear removes visible output when preserve logs is off. +- [x] Download creates a text download from the visible panel content. +- [x] Hide collapses the bottom panel without overlaying editor or side panels. +- [x] Execute reopens Output when hidden. + +## Mock Inventory Impact + +- Expected mock entries: + - Problems tab + - Terminal tab +- Parent plan already contains the Problems and Terminal mock entries. No additional mocked bottom-panel controls were introduced beyond those entries. + +## Verification + +- [x] Run `timeout 300s sbt "hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide"`. +- [x] Run `git diff --check`. +- [x] Run `git status --short` and confirm only intended files changed. +- [x] Browser-check Output, Problems, and Terminal tab switching. +- [x] Browser-check Preserve logs and Clear behavior. +- [x] Browser-check Download behavior. +- [x] Browser-check Hide and Execute-reopen behavior. +- [x] Browser-check bottom panel occupies layout space and does not overlay editor content. + +## Completion Notes + +- Commit: this phase commit. +- Browser notes: Playwright CLI verified the `` exists, Output/Problems/Terminal tabs switch with ARIA selected state, Output receives runtime-style log messages, Preserve logs prevents Clear from deleting output, Clear removes output when Preserve logs is off, Download creates `mlscript-output.txt`, Problems renders three mock rows and dispatches `/main.mls:5`, Terminal accepts a mock `help` command and can be cleared, Hide collapses the panel, Execute reopens Output, and the editor bottom edge stays above the bottom panel. Screenshot: `docs/web-ide-ui-framework-screenshots/phase-05/bottom-panel-1920x1080.png`. +- Test output: `hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide` passed 25 tests. diff --git a/docs/web-ide-ui-framework-phases/phase-06-editor-workbench-chrome.md b/docs/web-ide-ui-framework-phases/phase-06-editor-workbench-chrome.md new file mode 100644 index 0000000000..1f3c0aeeb5 --- /dev/null +++ b/docs/web-ide-ui-framework-phases/phase-06-editor-workbench-chrome.md @@ -0,0 +1,60 @@ +# Phase 6: Editor Workbench Chrome Progress Tracker + +Parent plan: [MLscript Web IDE UI Framework Plan](../web-ide-ui-framework-plan.md) + +## Status + +- [ ] Not started +- [x] In progress +- [x] Browser verified +- [x] Tests passed +- [x] Committed + +## Goal + +Add prototype-like editor chrome and a framework-only compiled-output split view without changing core CodeMirror editor behavior. + +## Deliverables + +- [x] Polish the editor tab strip inside the new workbench shell. +- [x] Add a breadcrumbs row for the active editor context. +- [x] Add editor action buttons only when each has real or mocked behavior. +- [x] Add a compiled-output split view shell. +- [x] Add static/mock output for `mjs`, `wasm`, and `c` targets. +- [x] Add target selection controls. +- [x] Add Copy, Download, and Close controls for the compiled-output pane. +- [x] Update the Mock Inventory for the compiled output split view. + +## Visible Functionality + +- [x] Existing editor open, edit, tab switch, and scroll behavior still works. +- [x] Breadcrumbs reflect the active file or use a clearly mocked value. +- [x] Compiled split view opens and closes without destroying the current editor tab. +- [x] Target controls switch mock output content and selected state. +- [x] Copy writes the selected mock output to the clipboard or shows a visible failure message. +- [x] Download creates a downloadable mock artifact for the selected target. +- [x] Closing compiled view preserves editor scrollability and diagnostics. +- [x] No editor chrome control is dead. + +## Mock Inventory Impact + +- Expected mock entries: + - Compiled output split view +- Parent plan already contains the compiled output split view mock entry. Breadcrumbs are backed by the real active-file event, and the only editor action opens the documented compiled-output mock surface. + +## Verification + +- [x] Run `timeout 300s sbt "hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide"`. +- [x] Run `git diff --check`. +- [x] Run `git status --short` and confirm only intended files changed. +- [x] Browser-check editor open, edit, scroll, and tab switching. +- [x] Browser-check compiled split open/close behavior. +- [x] Browser-check target switching. +- [x] Browser-check Copy and Download controls. +- [x] Browser-check Compile, Execute, diagnostics, and bottom output still work with the split view closed and open. + +## Completion Notes + +- Commit: this phase commit. +- Browser notes: Playwright CLI verified active-file breadcrumbs, editor open/edit/tab switch behavior, active CodeMirror scroll behavior in a constrained viewport, split open/close without losing editor tabs, `mjs`/`wasm`/`c` target switching, Copy feedback, Download of `mlscript-output.mjs`, diagnostics after Compile, Execute output with the split open, and output after the split closes. Screenshot: `docs/web-ide-ui-framework-screenshots/phase-06/editor-workbench-chrome-1920x1080.png`. +- Test output: `hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide` passed 26 tests. diff --git a/docs/web-ide-ui-framework-phases/phase-07-native-dialogs.md b/docs/web-ide-ui-framework-phases/phase-07-native-dialogs.md new file mode 100644 index 0000000000..36feb932cc --- /dev/null +++ b/docs/web-ide-ui-framework-phases/phase-07-native-dialogs.md @@ -0,0 +1,61 @@ +# Phase 7: Native Dialogs Progress Tracker + +Parent plan: [MLscript Web IDE UI Framework Plan](../web-ide-ui-framework-plan.md) + +## Status + +- [ ] Not started +- [x] In progress +- [x] Browser verified +- [x] Tests passed +- [x] Committed + +## Goal + +Add native dialog-based command surfaces for command palette and sharing, using `` and MLscript custom elements without introducing a JavaScript framework. + +## Deliverables + +- [x] Add a `` custom element backed by ``. +- [x] Add a command palette button in the titlebar. +- [x] Add keyboard shortcut support for opening the command palette. +- [x] Add command search/filtering. +- [x] Add commands for current real actions where possible. +- [x] Add mocked or disabled future commands only when documented in the Mock Inventory. +- [x] Add a `` custom element backed by ``. +- [x] Add ZIP download behavior for the share dialog. + +## Visible Functionality + +- [x] Command palette opens from the titlebar button. +- [x] Command palette opens from the keyboard shortcut. +- [x] First useful field is focused when the palette opens. +- [x] Search input filters command rows. +- [x] Escape closes the palette through native dialog behavior. +- [x] Real commands dispatch real events. +- [x] Mock commands visibly change UI state or are disabled with clear titles. +- [x] Share dialog opens and closes. +- [x] Share dialog downloads the workspace as a ZIP. + +## Mock Inventory Impact + +- Expected mock entries: + - Command palette future commands +- Update the parent plan for every command that is visible but not backed by real functionality. + +## Verification + +- [x] Run `timeout 300s sbt --client "hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide"`. +- [x] Run `git diff --check`. +- [x] Run `git status --short` and confirm only intended files changed. +- [x] Browser-check command palette button and shortcut. +- [x] Browser-check search filtering and focus behavior. +- [x] Browser-check Escape and close behavior. +- [x] Browser-check each visible command against the functionality standard. +- [x] Browser-check share dialog open, close, and copy behavior. + +## Completion Notes + +- Commit: this phase commit. +- Browser notes: Playwright verified command palette opening from toolbar button and Cmd/Ctrl+K, search filtering, delayed focus on the search input, native Escape close behavior, real compile command dispatch, mock wasm target selection, disabled theme command title, share dialog open/close, share focus, and copy feedback. Console check reported 0 errors and 0 warnings. +- Test output: Focused web-ide package test passed with 27 tests. Screenshot captured at `docs/web-ide-ui-framework-screenshots/phase-07/native-dialogs-1920x1080.png`. diff --git a/docs/web-ide-ui-framework-phases/phase-08-integration-cleanup.md b/docs/web-ide-ui-framework-phases/phase-08-integration-cleanup.md new file mode 100644 index 0000000000..31fa587789 --- /dev/null +++ b/docs/web-ide-ui-framework-phases/phase-08-integration-cleanup.md @@ -0,0 +1,69 @@ +# Phase 8: Integration And Cleanup Progress Tracker + +Parent plan: [MLscript Web IDE UI Framework Plan](../web-ide-ui-framework-plan.md) + +## Status + +- [ ] Not started +- [x] In progress +- [x] Browser verified +- [x] Focused tests passed +- [x] Full tests passed +- [x] Committed + +## Goal + +Integrate the new UI framework with the current Web IDE runtime behavior, remove obsolete shell assumptions, and ensure every visible element is functional, mocked and documented, disabled with a reason, or removed. + +## Deliverables + +- [x] Update `main.mls` to query and coordinate the new workbench components. +- [x] Route diagnostics through ``. +- [x] Route output visibility through ``. +- [x] Route titlebar/workbench status through the new shell. +- [x] Keep compile, execute, terminate, file open, diagnostics, and output flows working together. +- [x] Remove old placeholder panels and old right-rail assumptions. +- [x] Audit all visible controls for functionality. +- [x] Update the Mock Inventory to match the final visible mock surfaces. + +## Visible Functionality + +- [x] No visible dead controls remain. +- [x] Real compile flow works. +- [x] Real execute flow works. +- [x] Execute opens Output when hidden. +- [x] Real diagnostics appear and can navigate to file locations where supported. +- [x] File explorer opens files. +- [x] Editor remains editable for writable files and readonly for readonly files. +- [x] Left panel switching works. +- [x] Bottom panel switching works. +- [x] Diagnostics inspector modes work. +- [x] Command palette and share dialogs work according to their documented real or mocked behavior. +- [x] Mock-only panels are visibly coherent and do not block real editor workflows. + +## Mock Inventory Impact + +- Expected action: reconcile the parent plan's Mock Inventory with the final UI. +- Remove inventory rows for mocks that were replaced by real functionality. +- Add inventory rows for any remaining mocked or disabled future actions. +- Do not commit Phase 8 until the Mock Inventory matches the screen. +- The parent plan Mock Inventory still matches the final visible mock surfaces: Source Control, Outline, Examples, Problems, Terminal, compiled-output split view, and command palette future commands remain intentionally mocked or disabled. +- Search was realized after this phase. + +## Verification + +- [x] Run `timeout 300s sbt --client "hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide"`. +- [x] Run `git diff --check`. +- [x] Run `git status --short` and confirm only intended files changed. +- [x] Browser-check every visible control using the functionality standard. +- [x] Browser-check baseline workflow: open file, edit, compile, inspect diagnostics, execute, inspect output. +- [x] Browser-check desktop and narrow viewport layouts. +- [x] Browser-check no console errors were introduced. +- [x] Run `timeout 1800s sbt --client "hkmc2AllTests/test"`. + +## Completion Notes + +- Commit: this phase commit. +- Browser notes: Playwright CLI verified custom element registration, removal of obsolete `reserved-panel`/`console-panel` DOM and CSS variable usage, file explorer open flow, real `.mls` compile, disabled Compile on `.mjs`, Execute reopening Output, left panel switching and hide/reopen, diagnostics mode switching and hide/reopen, bottom tabs, compiled-output mock split, command palette filtering, share ZIP download, file/editor scrolling, editable std files, sidebar resize handles bounded above the bottom panel, desktop no horizontal overflow, narrow viewport side-panel auto-close with a usable editor width, and 0 console errors/warnings. Screenshot captured at `docs/web-ide-ui-framework-screenshots/phase-08/integration-cleanup-1920x1080.png`. +- Focused test output: `hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide` passed 25 tests. +- Full test output: `hkmc2AllTests/test` passed 574 tests. diff --git a/docs/web-ide-ui-framework-plan.md b/docs/web-ide-ui-framework-plan.md new file mode 100644 index 0000000000..2b220c6f4c --- /dev/null +++ b/docs/web-ide-ui-framework-plan.md @@ -0,0 +1,404 @@ +# MLscript Web IDE UI Framework Plan + +## Summary + +Build the prototype-inspired UI framework in phases, using MLscript custom elements and native HTML primitives. Phase 1 prioritizes shell/layout architecture with mocked secondary panels, not real Git/Outline/Terminal functionality. Existing editor, file explorer, compile, execute, diagnostics, and console behavior should remain working where practical. + +## Key Changes + +- Replace the current page-level layout with an MLscript-owned workbench shell custom element. +- Adopt the prototype structure: titlebar, left activity rail, left panel host, editor workbench, right diagnostics inspector, bottom panel, status bar. +- Use native HTML patterns: + - `` for command palette and share dialog. + - `
/` for static trees/groups. + - ``, checkboxes, radio groups, and buttons for controls. + - `hidden`, `inert`, `aria-selected`, `aria-pressed`, and `role="tablist"`/`role="tab"` where appropriate. +- Do not introduce React, JSX, or another frontend framework. +- Treat the Claude Design prototype as a visual and interaction reference only. + +## Functionality Standard + +Every visible control must be functional in the phase where it appears. + +- Real controls must dispatch the current Web IDE event or call the current component method. +- Framework-only controls must still change UI state, open/close a native surface, switch a tab, select a mode, copy mock text, clear mock content, or show a disabled state with a clear title. +- Nonfunctional placeholder buttons are not acceptable. If an action cannot be made useful in the current phase, render it disabled with `aria-disabled="true"` and a title explaining that the feature is mocked. +- Mocked surfaces must be internally consistent. Counts, badges, selected states, and visible content should agree with the mock data shown on screen. +- Keyboard and focus behavior counts as functionality for dialogs and tabbed surfaces: Escape closes dialogs, the first useful field receives focus, and tab selections update ARIA state. +- Maintain the mock inventory in this document. Every time a non-real UI element, mocked dataset, disabled future action, or framework-only behavior is added, document it in the Mock Inventory before committing that phase. + +## Mock Inventory + +This list tracks visible UI that is intentionally not backed by real functionality yet. Each phase must update this list when it adds, changes, or removes mocked UI. + +| UI surface | Phase added | Mocked behavior | Required real replacement | +| --- | --- | --- | --- | +| Source Control panel | Phase 3 | Static changed-file list with stage/unstage movement between mock sections. | Real version-control state, staging, commit, pull, and push integration if supported by the Web IDE environment. | +| Outline panel | Phase 3 | Static symbol list with mocked editor navigation. | Real symbol extraction from the active MLscript file. | +| Examples panel | Phase 3 | Static examples list with mocked selection/detail behavior. | Real bundled examples/snippets that can open or load files. | +| Problems tab | Phase 5 | Mocked problem list separate from current diagnostics. | Real diagnostic/problem aggregation from compiler results. | +| Terminal tab | Phase 5 | Static terminal transcript or locally mutable mock lines. | Real terminal/REPL integration, or remove if unsupported. | +| Compiled output split view | Phase 6 | Static `.mjs`/`wasm`/`c` mock output selected by target controls. | Real generated-output preview based on current compiled file and selected backend. | +| Command palette future commands | Phase 7 | Commands without current runtime support switch mock UI state or render disabled. | Real command implementations or removal from the palette. | + +## Progress Trackers + +- [Phase 1: Workbench Shell](web-ide-ui-framework-phases/phase-01-workbench-shell.md) +- [Phase 2: Visual Foundation](web-ide-ui-framework-phases/phase-02-visual-foundation.md) +- [Phase 3: Left Activity Panels](web-ide-ui-framework-phases/phase-03-left-activity-panels.md) +- [Phase 4: Diagnostics Inspector](web-ide-ui-framework-phases/phase-04-diagnostics-inspector.md) +- [Phase 5: Bottom Panel](web-ide-ui-framework-phases/phase-05-bottom-panel.md) +- [Phase 6: Editor Workbench Chrome](web-ide-ui-framework-phases/phase-06-editor-workbench-chrome.md) +- [Phase 7: Native Dialogs](web-ide-ui-framework-phases/phase-07-native-dialogs.md) +- [Phase 8: Integration And Cleanup](web-ide-ui-framework-phases/phase-08-integration-cleanup.md) + +## Phase Plan + +### Phase 1: Workbench Shell + +Create the structural framework. + +- Add a top-level custom element, such as ``, that owns: + - titlebar + - left activity rail + - left panel host + - editor region + - right diagnostics inspector region + - bottom panel region + - status bar +- Move sidebar tab switching out of `main.mls` into the shell component. +- Keep `` and `` embedded as real existing components. +- Replace the current right activity rail with a permanent diagnostics inspector region. +- Preserve current compile/execute events from the toolbar path, but route them through the new titlebar controls. +- Make each visible shell control functional: + - Compile dispatches `compile-requested`. + - Execute dispatches `execute-requested` and opens the output area if present. + - left rail buttons switch/hide panels. + - diagnostics hide/show control toggles the inspector. + - status bar segments are buttons only if they open or switch a visible surface; otherwise render as plain text. + +Commit: `Add IDE workbench shell` + +### Phase 2: Visual Tokens And Layout Polish + +Establish the design system before adding more panels. + +- Add CSS tokens for: + - warm light theme + - dark theme scaffold + - rail width + - panel width + - bottom panel height + - status bar height + - border, shadow, radius, diagnostic colors +- Restyle existing panels to match the prototype density: + - compact headers + - subtle borders + - muted panel backgrounds + - active tab and rail states +- Ensure editor, side panels, right inspector, and bottom panel all occupy real grid/flex space and scroll independently. +- Verify the theme toggle works if shown. If dark mode is not implemented in this phase, do not render a clickable theme button yet. + +Commit: `Add IDE visual foundation` + +### Phase 3: Left Activity Panels With Framework Data + +Build the left-side framework using real Files/Search surfaces and static data for the remaining exploratory panels. + +- Keep Files backed by the existing ``. +- Add custom elements for: + - Search + - Source Control + - Outline + - Examples +- Use a small `mockWorkbenchData.mls` module for static sample entries in Source Control, Outline, and Examples. +- Each panel should be interactive enough to prove the shell: + - rail button switches panels + - active state updates + - current panel can be hidden by clicking active rail item + - panel content scrolls +- Required mocked interactions: + - Source Control stage/unstage buttons move mock files between sections. + - Outline entries move the editor scroll position or dispatch a mocked navigation event. + - Examples selection marks the selected example and can open a mock preview or replace mock panel detail. +- Required real interactions: + - Search query filters workspace file contents and clear button empties the query. +- Do not implement real git, symbol extraction, or examples loading yet. + +Commit: `Add mock left activity panels` + +### Phase 4: Right Diagnostics Inspector Framework + +Rework diagnostics into the prototype-style inspector. + +- Replace `` with a diagnostics-focused custom element, such as ``. +- Provide three native radio/segmented modes: + - List + - Tree + - Source +- Render an empty state before compiler diagnostics are available. +- Preserve a `setDiagnostics(diagnosticsPerFile)` method so current compiler diagnostics can still be routed later. +- Show severity counts in the inspector header. +- Source mode should use cards with real diagnostic excerpts, locations, and supported actions. +- Required interactions: + - List, Tree, and Source mode buttons switch visible content and update selected state. + - Diagnostic rows/cards dispatch `open-file-at-location` when clicked if they point at an existing file. + - Hide diagnostics collapses the inspector and exposes a clear way to reopen it. + +Commit: `Add diagnostics inspector framework` + +### Phase 5: Bottom Panel Framework + +Replace the single console surface with a tabbed bottom panel. + +- Add `` with tabs: + - Output + - Problems + - Terminal +- Output can wrap or reuse the existing console stream initially. +- Problems and Terminal use mock content. +- Keep Preserve logs, Clear, Download, and Hide controls as native controls/buttons. +- Maintain the existing requirement: Execute opens the Output tab if the bottom panel is hidden. +- Required interactions: + - Output, Problems, and Terminal tabs switch content and ARIA selected state. + - Preserve logs checkbox changes the clear behavior for the output panel. + - Clear removes visible output or mock terminal lines when preserve logs is off. + - Download creates a downloadable text blob from the visible panel content. + - Hide collapses the bottom panel without overlaying the editor. + +Commit: `Add bottom panel framework` + +### Phase 6: Editor Workbench Chrome + +Add prototype-like editor chrome without changing editor semantics. + +- Keep CodeMirror/editor behavior intact. +- Add: + - tab strip polish + - breadcrumbs row + - editor action buttons + - optional compiled-output split view shell +- The compiled split view is framework-only: + - static/mock `.mjs` content + - target radio buttons for `mjs / wasm / c` + - copy/download/close buttons wired only for UI state +- Required interactions: + - split view opens and closes without destroying the current editor tab. + - target buttons switch mock output content and selected state. + - copy writes mock output text to the clipboard when browser permissions allow it, otherwise shows a visible failure message. + - download creates a downloadable mock artifact. +- Do not implement real generated-output preview in this phase. + +Commit: `Add editor workbench chrome` + +### Phase 7: Native Dialogs And Command Surfaces + +Add modal command surfaces with native HTML. + +- Add `` backed by ``. +- Open via titlebar button and keyboard shortcut. +- Include static commands: + - Compile current file + - Run compiled output + - Change compile target + - Go to symbol + - Go to file + - Search across files + - Toggle theme + - Toggle diagnostics panel + - Toggle console panel +- Add a small `` using ``. +- Escape and dialog close behavior should be native. +- Required interactions: + - Command palette opens from button and keyboard shortcut. + - Search input filters command rows. + - Commands that map to existing behavior dispatch the real events. + - Mock commands switch the relevant mocked UI state. + - Share dialog opens, closes, and downloads the workspace as a ZIP. + +Commit: `Add native command dialogs` + +### Phase 8: Integration And Cleanup + +Make the framework coherent with current runtime behavior. + +- Update `main.mls` to query the new components: + - diagnostics inspector for diagnostics + - bottom panel for output visibility + - titlebar/workbench for status updates +- Keep current compile/execute/run behavior working. +- Remove obsolete placeholder panels and old right-rail assumptions. +- Keep mock-only panels clearly isolated so real functionality can replace their data later. +- Remove or disable any visible action that still does nothing after integration. + +Commit: `Integrate IDE workbench shell` + +## Public Interfaces + +- `` owns panel selection and layout state. +- `.setDiagnostics(diagnosticsPerFile)` remains the diagnostics entrypoint. +- `.showPanel(panelName)` supports at least `"output"`, `"problems"`, and `"terminal"`. +- Existing events remain valid: + - `compile-requested` + - `execute-requested` + - `terminate-requested` + - `active-tab-changed` + - `open-file-at-location` +- Add one new event only if needed: + - `bottom-panel-open-requested` with `{ panel: "output" | "problems" | "terminal" }`. + +## Test Workflow + +Reuse the package-focused loop from `docs/web-ide-mlscript-second-pass-workflow.md`, but make browser verification stricter because this work changes visible behavior. + +### Per-Phase Loop + +1. Implement only one phase at a time. +2. Compile the package: + + ```sh + timeout 300s sbt "hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide" + ``` + +3. Check diff hygiene: + + ```sh + git diff --check + git status --short + ``` + + No unrelated golden snapshots or unrelated test files should change. + If the phase adds or changes any mocked UI, verify the Mock Inventory was updated in the same commit. + +4. Start a local server from the Web IDE package directory: + + ```sh + cd hkmc2/shared/src/test/mlscript-packages/web-ide + python3 -m http.server 8123 --bind 127.0.0.1 + ``` + +5. Open `http://127.0.0.1:8123/index.html?` with the Playwright CLI. +6. Verify no browser console errors were introduced. +7. Run the phase-specific browser checklist below. +8. Capture a 1920x1080 screenshot for any new or substantially changed UI surface and save it under `docs/web-ide-ui-framework-screenshots//`. +9. Commit the phase only after compile, diff hygiene, and browser checks pass. + +### Baseline Browser Checks + +Run these after every phase: + +- Page loads to the workbench without blank regions or horizontal document overflow. +- Files are visible in the explorer. +- A source file opens in the editor. +- Editor content scrolls independently from the panels. +- Compile works on an `.mls` file. +- Compile is skipped or disabled for a non-`.mls` file. +- Execute works and opens the Output tab/panel if hidden. +- Existing diagnostics still appear after compilation. +- Bottom panel occupies layout space and does not overlay editor or side panels. +- Resizing side and bottom panels does not expose handles over unrelated regions. + +### Visible-Control Checks + +For each phase, build a small manual checklist from every visible button, checkbox, tab, input, segmented control, dialog close button, and status-bar action added in that phase. + +Each item must satisfy one of these outcomes: + +- It performs a real Web IDE action. +- It performs a mocked UI action with visible state change. +- It is disabled and communicates why via title/label. +- It has been removed until the phase that can make it functional. + +The phase is not complete until every visible control has one of those outcomes. + +### Phase-Specific Browser Checks + +Phase 1: + +- Titlebar Compile and Execute dispatch the same behavior as the old toolbar. +- Left rail switches Files and any shell panels added in this phase. +- Clicking the active left rail button hides and reopens the left panel. +- Right diagnostics inspector hides and reopens. +- Status bar does not contain clickable dead controls. + +Phase 2: + +- Light theme renders with readable contrast. +- Dark theme renders if the theme button is visible. +- Layout remains stable at desktop width and a narrow viewport. +- Text does not overlap controls in titlebar, side panels, diagnostics, bottom panel, or status bar. + +Phase 3: + +- Search input filters workspace results. +- Search clear button clears the query. +- Source Control mock stage/unstage changes visible sections and counts. +- Outline entry activation changes visible editor position or dispatches a visible navigation signal. +- Example selection changes selected state and detail/preview. + +Phase 4: + +- Diagnostics List, Tree, and Source modes switch content and selected state. +- Severity counts match visible real diagnostics. +- Diagnostic click opens the relevant file/line when backed by a real file. + +Phase 5: + +- Output, Problems, and Terminal tabs switch content. +- Preserve logs changes Clear behavior. +- Clear removes visible output when allowed. +- Download creates a text download from visible content. +- Hide collapses the panel; Execute reopens Output. + +Phase 6: + +- Compiled split view opens and closes. +- Target selector switches mock content. +- Copy and Download act on the currently selected mock output. +- Closing compiled view preserves editor tab, scrollability, and diagnostics. + +Phase 7: + +- Command palette opens from button and shortcut. +- Command search filters rows. +- Escape closes the dialog. +- Real commands dispatch real events; mock commands change visible UI state. +- Share dialog opens, closes, and downloads the workspace as a ZIP. + +Phase 8: + +- No visible dead controls remain. +- Real compile, execute, diagnostics, file open, panel switching, and bottom output flow still work together. +- Mock-only panels are visually clear as framework surfaces without blocking real editor workflows. + +### Automated/Scripted Checks + +When practical, add lightweight browser scripts or test harness snippets that assert: + +- required custom elements are defined; +- expected regions exist exactly once; +- selected/hidden ARIA states match visible panels; +- panel scroll containers have `scrollHeight > clientHeight` for long mock data; +- clicking each rail/tab/mode control changes the expected state; +- Execute opens the Output panel when hidden. + +Keep these scripts as verification helpers unless the package test infrastructure already has a natural place for browser UI tests. + +### Test Commands + +- Run focused package test after each phase: + - `sbt "hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide"` +- Run diff checks before every commit: + - `git diff --check` + - `git status --short` +- Before final completion: + - `sbt hkmc2AllTests/test` + +## Assumptions + +- Phase 1 target is the native shell framework, not full visual parity. +- Right side becomes a diagnostics inspector, not a right activity rail. +- Source Control, Outline, Examples, Problems, Terminal, and compiled preview use mock data until later phases. +- Search uses real workspace text search after its realization pass. +- Existing editor, file explorer, compile, execute, and diagnostics should remain usable unless a phase explicitly replaces their wrapper UI. +- No React, JSX, or frontend framework dependencies are introduced. diff --git a/hkmc2/js/src/main/scala/hkmc2/Compiler.scala b/hkmc2/js/src/main/scala/hkmc2/Compiler.scala index 8cb84b181b..44053bf20a 100644 --- a/hkmc2/js/src/main/scala/hkmc2/Compiler.scala +++ b/hkmc2/js/src/main/scala/hkmc2/Compiler.scala @@ -11,6 +11,7 @@ import scala.collection.immutable import scala.collection.mutable.Map as MutMap import io.* +import analysis.* import scala.collection.mutable.{ArrayBuffer, Buffer} @JSExportTopLevel("Compiler") @@ -23,17 +24,23 @@ class Compiler(paths: MLsCompiler.Paths)(using cctx: CompilerCtx): pathDiagnosticsMap.getOrElseUpdate(path.toString, (pathDiagnosticsMap.size, Buffer.empty))._2 += d private val compiler = MLsCompiler(paths, mkRaise) - + private val analyzer = Analyzer(paths, mkRaise) + + private def collectDiagnosticFiles(): Ls[FileDiagnostics] = + pathDiagnosticsMap.toList.sortBy(_._2._1).map: + case (path, (_, diagnostics)) => + FileDiagnostics(path, diagnostics.toList) + private def collectDiagnostics(): js.Array[js.Dynamic] = - pathDiagnosticsMap.toArray.sortBy(_._2._1).map: - case (path, (_, diagnostics)) => js.Dynamic.literal( - path = path, - diagnostics = diagnostics.iterator.map: d => + collectDiagnosticFiles().map: file => + js.Dynamic.literal( + path = file.path, + diagnostics = file.diagnostics.map: d => js.Dynamic.literal( kind = d.kind.toString().toLowerCase(), source = d.source.toString().toLowerCase(), mainMessage = d.theMsg, - allMessages = d.allMsgs.iterator.map: + allMessages = d.allMsgs.map: case (message, loc) => lazy val ctx = ShowCtx.mk: message.bits.collect: @@ -54,16 +61,48 @@ class Compiler(paths: MLsCompiler.Paths)(using cctx: CompilerCtx): ) .toJSArray) .toJSArray - + + private def resetDiagnostics(): Unit = + pathDiagnosticsMap = MutMap.empty + @JSExport def compile(filePath: Str): js.Array[js.Dynamic] = compiler.compileModule(Path(filePath)) val perFileDiagnostics = collectDiagnostics() - pathDiagnosticsMap = MutMap.empty + resetDiagnostics() perFileDiagnostics + def analyzeDocument(filePath: Str): AnalysisDocument = + val document = analyzer.analyze(Path(filePath)) + val result = document.copy(diagnostics = collectDiagnosticFiles()) + resetDiagnostics() + result + + @JSExport + def analyze(filePath: Str): js.Dynamic = + AnalysisJsCodec.document(analyzeDocument(filePath)) + +/** JS-facing wrapper for browser workers. + * + * The regular `Compiler` needs a Scala `CompilerCtx`. This wrapper builds it + * from the browser's virtual filesystem. Module resolution is delegated to + * `WebModuleResolver`, which is currently minimal. + */ +@JSExportTopLevel("BrowserCompiler") +class BrowserCompiler(fs: DummyFileSystem, paths: MLsCompiler.Paths): + private given CompilerCtx = CompilerCtx.fresh(fs, WebModuleResolver()) + private val compiler = new Compiler(paths) + + @JSExport + def compile(filePath: Str): js.Array[js.Dynamic] = + compiler.compile(filePath) + + @JSExport + def analyze(filePath: Str): js.Dynamic = + compiler.analyze(filePath) + @JSExportTopLevel("Paths") -final class Paths(prelude: Str, runtime: Str, runtimeSource: Str, term: Str) extends MLsCompiler.Paths: +final class Paths(prelude: Str, runtime: Str, runtimeSource: Str, term: Str, std: Str) extends MLsCompiler.Paths: val preludeFile = Path(prelude) val runtimeFile = Path(runtime) val runtimeSourceFile = Path(runtimeSource) diff --git a/hkmc2/js/src/main/scala/hkmc2/WebModuleResolver.scala b/hkmc2/js/src/main/scala/hkmc2/WebModuleResolver.scala new file mode 100644 index 0000000000..1abe0070d1 --- /dev/null +++ b/hkmc2/js/src/main/scala/hkmc2/WebModuleResolver.scala @@ -0,0 +1,14 @@ +package hkmc2 + +import hkmc2.utils.*, shorthands.* +import ModuleResolver.* + +/** Browser resolver for imports that should not be treated as virtual files. + * + * Returning `N` still makes imports fall back to normal file-path resolution + * against the worker's virtual filesystem. Package rules should be added here. + */ +class WebModuleResolver(using fs: io.FileSystem) extends ModuleResolver: + + def tryResolveModulePath(path: Str): Opt[ResolvedModule] = + ModuleResolver.tryResolveUrl(path) diff --git a/hkmc2/js/src/main/scala/hkmc2/analysis/AnalysisJsCodec.scala b/hkmc2/js/src/main/scala/hkmc2/analysis/AnalysisJsCodec.scala new file mode 100644 index 0000000000..9bad9cd2a7 --- /dev/null +++ b/hkmc2/js/src/main/scala/hkmc2/analysis/AnalysisJsCodec.scala @@ -0,0 +1,111 @@ +package hkmc2.analysis + +import scala.scalajs.js +import js.JSConverters.* + +import hkmc2.{Diagnostic, Message, ShowCtx} +import hkmc2.utils.*, shorthands.* + +object AnalysisJsCodec: + + def document(document: AnalysisDocument): js.Dynamic = + val result = js.Dynamic.literal( + schema = document.schema, + version = document.version, + rootFile = document.rootFile, + root = document.root.map(symbolNode).orNull, + diagnostics = document.diagnostics.map(fileDiagnostics).toJSArray, + dependencies = document.dependencies.map(dependencyInfo).toJSArray, + ) + document.revision.foreach: revision => + result.updateDynamic("revision")(revision) + result + + private def symbolNode(node: SymbolNode): js.Dynamic = + val result = js.Dynamic.literal( + id = node.id, + stableId = node.stableId, + name = node.name, + displayName = node.displayName, + kind = node.kind.wireName, + origin = node.origin.wireName, + range = node.range.map(sourceRange).orNull, + selectionRange = node.selectionRange.map(sourceRange).orNull, + flags = symbolFlags(node.flags), + children = node.children.map(symbolNode).toJSArray, + ) + node.signature.foreach: signature => + result.updateDynamic("signature")(signature) + node.detail.foreach: detail => + result.updateDynamic("detail")(detail) + node.source.foreach: source => + result.updateDynamic("source")(sourceInfo(source)) + result + + private def sourceRange(range: SourceRange): js.Dynamic = + js.Dynamic.literal( + file = range.file, + start = range.start, + end = range.end, + startLine = range.startLine, + startColumn = range.startColumn, + endLine = range.endLine, + endColumn = range.endColumn, + ) + + private def symbolFlags(flags: SymbolFlags): js.Dynamic = + js.Dynamic.literal( + exported = flags.exported, + overloaded = flags.overloaded, + mutable = flags.mutable, + hasBody = flags.hasBody, + ) + + private def sourceInfo(source: SourceInfo): js.Dynamic = + js.Dynamic.literal( + treeKind = source.treeKind, + description = source.description, + ) + + private def dependencyInfo(info: DependencyInfo): js.Dynamic = + val result = js.Dynamic.literal( + file = info.file, + kind = info.kind, + range = info.range.map(sourceRange).orNull, + ) + info.importedName.foreach: importedName => + result.updateDynamic("importedName")(importedName) + result + + private def fileDiagnostics(file: FileDiagnostics): js.Dynamic = + js.Dynamic.literal( + path = file.path, + diagnostics = file.diagnostics.map(diagnostic).toJSArray, + ) + + private def diagnostic(diagnostic: Diagnostic): js.Dynamic = + js.Dynamic.literal( + kind = diagnostic.kind.toString().toLowerCase(), + source = diagnostic.source.toString().toLowerCase(), + mainMessage = diagnostic.theMsg, + allMessages = diagnostic.allMsgs.map: + case (message, loc) => + lazy val ctx = ShowCtx.mk: + message.bits.collect: + case Message.Code(t) => t + js.Dynamic.literal( + messageBits = message.bits.map: + case Message.Text(text) => js.Dynamic.literal(text = text) + case Message.Code(ty) => ty.showIn(0)(using ctx) + .toJSArray, + location = loc match + case S(loc) => js.Dynamic.literal( + start = loc.spanStart, + end = loc.spanEnd, + ) + case N => null + ) + .toJSArray, + ) + +end AnalysisJsCodec diff --git a/hkmc2/js/src/main/scala/hkmc2/analysis/AnalysisSchema.scala b/hkmc2/js/src/main/scala/hkmc2/analysis/AnalysisSchema.scala new file mode 100644 index 0000000000..ae1e470207 --- /dev/null +++ b/hkmc2/js/src/main/scala/hkmc2/analysis/AnalysisSchema.scala @@ -0,0 +1,100 @@ +package hkmc2.analysis + +import hkmc2.{Diagnostic, Loc} +import hkmc2.utils.*, shorthands.* + +enum SymbolKind(val wireName: Str): + case File extends SymbolKind("file") + case Module extends SymbolKind("module") + case Object extends SymbolKind("object") + case Class extends SymbolKind("class") + case Trait extends SymbolKind("trait") + case Mixin extends SymbolKind("mixin") + case Type extends SymbolKind("type") + case Pattern extends SymbolKind("pattern") + case Constructor extends SymbolKind("constructor") + case Function extends SymbolKind("function") + case Value extends SymbolKind("value") + case MutableValue extends SymbolKind("mutable-value") + case Parameter extends SymbolKind("parameter") + case Field extends SymbolKind("field") + case Unknown extends SymbolKind("unknown") + +enum SymbolOrigin(val wireName: Str): + case Source extends SymbolOrigin("source") + case Import extends SymbolOrigin("import") + case Synthetic extends SymbolOrigin("synthetic") + case Error extends SymbolOrigin("error") + +final case class SourceRange( + file: Str, + start: Int, + end: Int, + startLine: Int, + startColumn: Int, + endLine: Int, + endColumn: Int, +) +object SourceRange: + def fromLoc(loc: Loc): SourceRange = + val (startLine, _, startColumn) = loc.origin.fph.getLineColAt(loc.spanStart) + val (endLine, _, endColumn) = loc.origin.fph.getLineColAt(loc.spanEnd) + SourceRange( + loc.origin.fileName.toString, + loc.spanStart, + loc.spanEnd, + loc.origin.startLineNum + startLine, + startColumn, + loc.origin.startLineNum + endLine, + endColumn, + ) + +final case class DependencyInfo( + file: Str, + kind: Str, + importedName: Opt[Str], + range: Opt[SourceRange], +) + +final case class SymbolFlags( + exported: Bool, + overloaded: Bool, + mutable: Bool, + hasBody: Bool, +) + +final case class SourceInfo( + treeKind: Str, + description: Str, +) + +final case class SymbolNode( + id: Str, + stableId: Str, + name: Str, + displayName: Str, + kind: SymbolKind, + origin: SymbolOrigin, + range: Opt[SourceRange], + selectionRange: Opt[SourceRange], + signature: Opt[Str], + detail: Opt[Str], + flags: SymbolFlags, + children: Ls[SymbolNode], + source: Opt[SourceInfo], +) + +final case class FileDiagnostics( + path: Str, + diagnostics: Ls[Diagnostic], +) + +final case class AnalysisDocument( + schema: Str, + version: Int, + revision: Opt[Int], + rootFile: Str, + root: Opt[SymbolNode], + diagnostics: Ls[FileDiagnostics], + dependencies: Ls[DependencyInfo], +) diff --git a/hkmc2/js/src/main/scala/hkmc2/analysis/Analyzer.scala b/hkmc2/js/src/main/scala/hkmc2/analysis/Analyzer.scala new file mode 100644 index 0000000000..64a13b7f32 --- /dev/null +++ b/hkmc2/js/src/main/scala/hkmc2/analysis/Analyzer.scala @@ -0,0 +1,63 @@ +package hkmc2.analysis + +import hkmc2.* +import hkmc2.io +import hkmc2.semantics.* +import hkmc2.semantics.Elaborator.{Ctx, State} +import hkmc2.utils.*, shorthands.* + +class Analyzer(paths: MLsCompiler.Paths, mkRaise: io.Path => Raise)(using cctx: CompilerCtx, config: Config): + import paths.* + + private var dbgParsing = false + private var dbgElab = false + + def analyze(file: io.Path): AnalysisDocument = + val wd = file.up + + given Raise = mkRaise(file) + + given Elaborator.State = new Elaborator.State: + override def dbg: Bool = dbgElab + + given SymbolPrinter = new SymbolPrinter( + Scope.empty(Scope.Cfg.default.copy( + escapeChars = false, + useSuperscripts = true, + includeZero = true, + )) + ) + + val etl = new TraceLogger: + override def doTrace: Bool = false + val rtl = new TraceLogger: + override def doTrace: Bool = false + + 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) + val parsed = mainParse.resultBlk + val (blk0, _) = elab.importFrom(parsed) + Config.extractConfigFromStats(blk0).givenIn: + val resolver = Resolver(rtl) + resolver.traverseBlock(blk0)(using Resolver.ICtx.empty) + val builder = SymbolTreeBuilder(file, mainParse.origin, parsed, blk0) + AnalysisDocument( + "mlscript.symbol-tree", + 1, + N, + file.toString, + S(builder.buildRoot()), + Nil, + builder.dependencies(), + ) + +end Analyzer diff --git a/hkmc2/js/src/main/scala/hkmc2/analysis/SymbolTreeBuilder.scala b/hkmc2/js/src/main/scala/hkmc2/analysis/SymbolTreeBuilder.scala new file mode 100644 index 0000000000..2da4b10fb4 --- /dev/null +++ b/hkmc2/js/src/main/scala/hkmc2/analysis/SymbolTreeBuilder.scala @@ -0,0 +1,238 @@ +package hkmc2.analysis + +import hkmc2.{Loc, Origin} +import hkmc2.io +import hkmc2.semantics.* +import hkmc2.syntax +import hkmc2.syntax.Tree +import hkmc2.utils.*, shorthands.* + +class SymbolTreeBuilder( + rootFile: io.Path, + origin: Origin, + parsed: Tree.Block, + elaborated: Term.Blk, +): + private var nextId: Int = 0 + private val rootFileName = rootFile.toString + + def buildRoot(): SymbolNode = + val stableId = s"file:$rootFileName" + SymbolNode( + allocateId(stableId), + stableId, + rootFile.last, + rootFile.toString, + SymbolKind.File, + SymbolOrigin.Source, + S(fileRange()), + S(fileRange()), + N, + N, + SymbolFlags(exported = true, overloaded = false, mutable = false, hasBody = true), + definitionNodes(elaborated.stats, stableId), + S(SourceInfo("Block", "file")), + ) + + def dependencies(): Ls[DependencyInfo] = + elaborated.stats.collect: + case imp: Import => + DependencyInfo( + imp.file.toString, + importKindName(imp.kind), + importedName(imp), + imp.toLoc.map(SourceRange.fromLoc), + ) + + private def allocateId(stableId: Str): Str = + nextId += 1 + s"$stableId#$nextId" + + private def fileRange(): SourceRange = + SourceRange.fromLoc(Loc(0, origin.fph.blockStr.length, origin)) + + private def definitionNodes(stats: Ls[Statement], ownerStableId: Str): Ls[SymbolNode] = + stats.zipWithIndex.flatMap: + case (statement, index) => + definitionNode(statement, ownerStableId, index) + + private def definitionNode(statement: Statement, ownerStableId: Str, index: Int): Opt[SymbolNode] = + statement match + case definition: TermDefinition => + S(termDefinitionNode(definition, ownerStableId, index)) + case definition: ClassLikeDef => + S(classLikeNode(definition, ownerStableId, index)) + case definition: hkmc2.semantics.TypeDef => + S(typeDefinitionNode(definition, ownerStableId, index)) + case _ => + N + + private def termDefinitionNode(definition: TermDefinition, ownerStableId: Str, index: Int): SymbolNode = + val stableId = memberStableId(ownerStableId, definition.sym.nme, kindForTerm(definition), definition.toLoc, index) + val parameterChildren = parameterNodes(definition.params, stableId) + val nestedChildren = definition.body.toList.flatMap(nestedDefinitionNodes(_, stableId)) + SymbolNode( + allocateId(stableId), + stableId, + definition.sym.nme, + definition.sym.nme, + kindForTerm(definition), + originFor(definition.sym.nameIsMeaningful, definition.toLoc), + definition.toLoc.map(SourceRange.fromLoc), + definition.tsym.toLoc.map(SourceRange.fromLoc).orElse(definition.toLoc.map(SourceRange.fromLoc)), + N, + S(termDetail(definition)), + SymbolFlags( + exported = isExported(definition.annotations), + overloaded = definition.sym.trees.distinct.length > 1, + mutable = definition.k is syntax.MutVal, + hasBody = definition.body.isDefined, + ), + parameterChildren ::: nestedChildren, + sourceInfo(definition.sym), + ) + + private def classLikeNode(definition: ClassLikeDef, ownerStableId: Str, index: Int): SymbolNode = + val stableId = memberStableId(ownerStableId, definition.bsym.nme, kindForClassLike(definition), definition.toLoc, index) + val parameterChildren = definition.paramsOpt.toList.flatMap(parametersInList(_, stableId, SymbolKind.Parameter)) + val memberChildren = definitionNodes(definition.body.blk.stats, stableId) + SymbolNode( + allocateId(stableId), + stableId, + definition.bsym.nme, + definition.bsym.nme, + kindForClassLike(definition), + originFor(definition.bsym.nameIsMeaningful, definition.toLoc), + definition.toLoc.map(SourceRange.fromLoc), + definition.sym.toLoc.map(SourceRange.fromLoc).orElse(definition.toLoc.map(SourceRange.fromLoc)), + N, + S(definition.kind.str), + SymbolFlags( + exported = isExported(definition.annotations), + overloaded = definition.bsym.trees.distinct.length > 1, + mutable = false, + hasBody = definition.body.blk.stats.nonEmpty, + ), + parameterChildren ::: memberChildren, + sourceInfo(definition.bsym), + ) + + private def typeDefinitionNode(definition: hkmc2.semantics.TypeDef, ownerStableId: Str, index: Int): SymbolNode = + val stableId = memberStableId(ownerStableId, definition.bsym.nme, SymbolKind.Type, definition.toLoc, index) + SymbolNode( + allocateId(stableId), + stableId, + definition.bsym.nme, + definition.bsym.nme, + SymbolKind.Type, + originFor(definition.bsym.nameIsMeaningful, definition.toLoc), + definition.toLoc.map(SourceRange.fromLoc), + definition.sym.toLoc.map(SourceRange.fromLoc).orElse(definition.toLoc.map(SourceRange.fromLoc)), + N, + S("type"), + SymbolFlags( + exported = isExported(definition.annotations), + overloaded = definition.bsym.trees.distinct.length > 1, + mutable = false, + hasBody = definition.rhs.isDefined, + ), + Nil, + sourceInfo(definition.bsym), + ) + + private def nestedDefinitionNodes(term: Term, ownerStableId: Str): Ls[SymbolNode] = + term match + case Term.Blk(stats, res) => + definitionNodes(stats, ownerStableId) ::: nestedDefinitionNodes(res, ownerStableId) + case _ => + term.subTerms.toList.flatMap(nestedDefinitionNodes(_, ownerStableId)) + + private def parameterNodes(params: Ls[ParamList], ownerStableId: Str): Ls[SymbolNode] = + params.zipWithIndex.flatMap: + case (paramList, listIndex) => + parametersInList(paramList, s"$ownerStableId/params:$listIndex", SymbolKind.Parameter) + + private def parametersInList(paramList: ParamList, ownerStableId: Str, kind: SymbolKind): Ls[SymbolNode] = + paramList.allParams.zipWithIndex.map: + case (param, index) => + val stableId = memberStableId(ownerStableId, param.sym.nme, kind, param.toLoc, index) + SymbolNode( + allocateId(stableId), + stableId, + param.sym.nme, + param.sym.nme, + kind, + originFor(param.sym.name.nonEmpty, param.toLoc), + param.toLoc.map(SourceRange.fromLoc), + param.sym.toLoc.map(SourceRange.fromLoc).orElse(param.toLoc.map(SourceRange.fromLoc)), + N, + N, + SymbolFlags(exported = false, overloaded = false, mutable = param.flags.mut, hasBody = false), + Nil, + N, + ) + + private def memberStableId( + ownerStableId: Str, + name: Str, + kind: SymbolKind, + loc: Opt[Loc], + index: Int, + ): Str = + val locationPart = loc.fold(s"index:$index")(loc => s"${loc.origin.fileName}:${loc.spanStart}:${loc.spanEnd}") + s"$ownerStableId/${kind.wireName}:$name@$locationPart" + + private def kindForTerm(definition: TermDefinition): SymbolKind = + definition.k match + case syntax.Fun => SymbolKind.Function + case syntax.MutVal => SymbolKind.MutableValue + case _: syntax.ValLike => SymbolKind.Value + case _ => SymbolKind.Unknown + + private def kindForClassLike(definition: ClassLikeDef): SymbolKind = + definition.kind match + case syntax.Mod => SymbolKind.Module + case syntax.Obj => SymbolKind.Object + case syntax.Cls => SymbolKind.Class + case syntax.Pat => SymbolKind.Pattern + + private def originFor(nameIsMeaningful: Bool, loc: Opt[Loc]): SymbolOrigin = + if !nameIsMeaningful then SymbolOrigin.Synthetic + else + loc match + case S(loc) if loc.origin.fileName.toString =/= rootFileName => SymbolOrigin.Import + case _ => SymbolOrigin.Source + + private def isExported(annotations: Ls[Annot]): Bool = + !annotations.exists: + case Annot.Modifier(syntax.Keyword.`private`) => true + case _ => false + + private def termDetail(definition: TermDefinition): Str = + definition.k match + case syntax.Fun => "function" + case syntax.MutVal => "mutable value" + case syntax.ImmutVal => "value" + case syntax.Ins => "implicit instance" + case syntax.LetBind => "let binding" + case syntax.HandlerBind => "handler binding" + + private def sourceInfo(symbol: BlockMemberSymbol): Opt[SourceInfo] = + symbol.trees.headOption.map: tree => + val treeKind = tree match + case _: Tree.TypeDef => "TypeDef" + case _: Tree.TermDef => "TermDef" + SourceInfo(treeKind, tree.describe) + + private def importKindName(kind: ImportKind): Str = + kind match + case ImportKind.Default => "default" + case ImportKind.Namespace => "namespace" + case ImportKind.Named(_) => "named" + + private def importedName(imp: Import): Opt[Str] = + imp.kind match + case ImportKind.Named(importedName) => S(importedName) + case _ => S(imp.sym.nme) + +end SymbolTreeBuilder diff --git a/hkmc2/js/src/main/scala/hkmc2/io/DummyFileSystem.scala b/hkmc2/js/src/main/scala/hkmc2/io/DummyFileSystem.scala new file mode 100644 index 0000000000..f5371b6584 --- /dev/null +++ b/hkmc2/js/src/main/scala/hkmc2/io/DummyFileSystem.scala @@ -0,0 +1,27 @@ +package hkmc2 +package io + +import hkmc2.utils.shorthands._ +import scala.scalajs.js +import scala.scalajs.js.annotation.JSExportTopLevel + +@js.native +trait JSFileSystem extends js.Object: + def read(path: String): String = js.native + def write(path: String, content: String): Unit = js.native + def exists(path: String): Boolean = js.native + def getLastChangedTimestamp(path: String): Double = js.native + +@JSExportTopLevel("DummyFileSystem") +class DummyFileSystem(fs: JSFileSystem) extends FileSystem: + def read(path: Path): String = + fs.read(path.toString) + + def write(path: Path, content: String): Unit = + fs.write(path.toString, content) + + def exists(path: Path): Bool = + fs.exists(path.toString) + + def getLastChangedTimestamp(path: Path): Long = + fs.getLastChangedTimestamp(path.toString).toLong diff --git a/hkmc2/js/src/main/scala/hkmc2/io/VirtualPath.scala b/hkmc2/js/src/main/scala/hkmc2/io/VirtualPath.scala index 7542438aed..634ebe99ae 100644 --- a/hkmc2/js/src/main/scala/hkmc2/io/VirtualPath.scala +++ b/hkmc2/js/src/main/scala/hkmc2/io/VirtualPath.scala @@ -111,3 +111,14 @@ private[io] class VirtualRelPath(val pathString: String) extends RelPath: else pathString + sep + other.toString new VirtualRelPath(combined) + + def last: String = + val idx = pathString.lastIndexOf(sep) + if idx < 0 then pathString + else pathString.substring(idx + 1) + + def baseName: String = + val filename = last + val dotIdx = filename.lastIndexOf('.') + if dotIdx <= 0 then filename // .hidden files or no extension + else filename.substring(0, dotIdx) diff --git a/hkmc2/js/src/main/scala/hkmc2/io/node/NodePath.scala b/hkmc2/js/src/main/scala/hkmc2/io/node/NodePath.scala index 0e3bfe8b9a..68a90aa49e 100644 --- a/hkmc2/js/src/main/scala/hkmc2/io/node/NodePath.scala +++ b/hkmc2/js/src/main/scala/hkmc2/io/node/NodePath.scala @@ -46,8 +46,12 @@ private[io] case class NodePath(val pathString: String) extends Path: private[io] class NodeRelPath(val pathString: String) extends RelPath: override def toString: String = pathString + private lazy val parsed = path.parse(pathString) + def segments: Ls[String] = pathString.split(path.sep).toList.filter(_.nonEmpty) def /(other: RelPath): RelPath = new NodeRelPath(path.join(pathString, other.toString)) + + def baseName: String = parsed.name diff --git a/hkmc2/js/src/test/scala/hkmc2/AnalysisTest.scala b/hkmc2/js/src/test/scala/hkmc2/AnalysisTest.scala new file mode 100644 index 0000000000..6fee144a39 --- /dev/null +++ b/hkmc2/js/src/test/scala/hkmc2/AnalysisTest.scala @@ -0,0 +1,70 @@ +package hkmc2 + +import org.scalatest.funsuite.AnyFunSuite +import io.{InMemoryFileSystem, Path, node} +import hkmc2.utils.*, shorthands.* + +class AnalysisTest extends AnyFunSuite: + val projectRoot = node.process.cwd() + val compilePath = node.path.join(projectRoot, "hkmc2", "shared", "src", "test", "mlscript-compile") + val runtimePath = node.path.join(projectRoot, "hkmc2", "shared", "src", "test", "mlscript-compile", "RuntimeJS.mjs") + val preludePath = node.path.join(projectRoot, "hkmc2", "shared", "src", "test", "mlscript", "decls", "Prelude.mls") + + private def loadStandardLibrary(): Map[String, String] = + node.fs.readdirSync(compilePath).filter: + fileName => fileName.endsWith(".mls") || fileName.endsWith(".mjs") + .toSeq.flatMap: fileName => + val filePath = node.path.join(compilePath, fileName) + if node.fs.existsSync(filePath) then + Some(s"/std/$fileName" -> node.fs.readFileSync(filePath, "utf-8")) + else + None + .toMap + + ("/std/RuntimeJS.mjs" -> node.fs.readFileSync(runtimePath, "utf-8")) + + ("/std/Prelude.mls" -> node.fs.readFileSync(preludePath, "utf-8")) + + private val paths = new Paths("/std/Prelude.mls", "/std/Runtime.mjs", "/std/Runtime.mls", "/std/Term.mjs", "/std") + + private def createCompiler(): (InMemoryFileSystem, Compiler) = + val fs = new InMemoryFileSystem(loadStandardLibrary()) + given CompilerCtx = CompilerCtx.fresh(fs, WebModuleResolver()) + (fs, new Compiler(paths)) + + private def child(node: analysis.SymbolNode, name: String): analysis.SymbolNode = + node.children.find(_.name === name).getOrElse: + fail(s"Expected child '$name' under '${node.name}', got ${node.children.map(_.name).mkString(", ")}") + + test("analyze returns an elaboration-backed recursive symbol tree without emitting JavaScript"): + val (fs, compiler) = createCompiler() + val inputPath = "/OutlineSample.mls" + val outputPath = "/OutlineSample.mjs" + + fs.write(inputPath, + """|module OutlineSample with + | class Box with + | fun get() = 1 + | + | object Tools with + | fun identity(x) = x + | + | fun top(x) = x + |""".stripMargin) + + val document = compiler.analyzeDocument(inputPath) + + assert(document.schema === "mlscript.symbol-tree") + assert(document.version === 1) + assert(document.rootFile === inputPath) + assert(document.root.isDefined) + assert(!fs.exists(Path(outputPath)), "Analysis should not emit a JavaScript output file") + + val root = document.root.get + assert(root.kind === analysis.SymbolKind.File) + val module = child(root, "OutlineSample") + assert(module.kind === analysis.SymbolKind.Module) + assert(child(module, "Box").kind === analysis.SymbolKind.Class) + assert(child(child(module, "Box"), "get").kind === analysis.SymbolKind.Function) + assert(child(module, "Tools").kind === analysis.SymbolKind.Object) + assert(child(child(module, "Tools"), "identity").kind === analysis.SymbolKind.Function) + assert(child(module, "top").kind === analysis.SymbolKind.Function) + assert(document.diagnostics.isEmpty) diff --git a/hkmc2/js/src/test/scala/hkmc2/CompilerTest.scala b/hkmc2/js/src/test/scala/hkmc2/CompilerTest.scala index ea33f95976..420fb4edef 100644 --- a/hkmc2/js/src/test/scala/hkmc2/CompilerTest.scala +++ b/hkmc2/js/src/test/scala/hkmc2/CompilerTest.scala @@ -8,34 +8,44 @@ import scala.scalajs.js.annotation._ import scala.scalajs.js.Dynamic.global class CompilerTest extends AnyFunSuite: + val projectRoot = node.process.cwd() + val compilePath = node.path.join(projectRoot, "hkmc2", "shared", "src", "test", "mlscript-compile") + val runtimePath = node.path.join(projectRoot, "hkmc2", "shared", "src", "test", "mlscript-compile", "RuntimeJS.mjs") + val preludePath = node.path.join(projectRoot, "hkmc2", "shared", "src", "test", "mlscript", "decls", "Prelude.mls") + private def loadStandardLibrary(): Map[String, String] = - val projectRoot = node.process.cwd() - val compilePath = node.path.join(projectRoot, "hkmc2", "shared", "src", "test", "mlscript-compile") - val preludePath = node.path.join(projectRoot, "hkmc2", "shared", "src", "test", "mlscript", "decls", "Prelude.mls") - node.fs.readdirSync(compilePath).filter(_.endsWith(".mls")).toSeq.flatMap: fileName => + // Actually, there's no need to load `.mjs` files. But we did it since we + // were importing them to reduce duplicated parsing and elaboration. The + // imports can be reverted back to `.mls` files, but we should check that + // doing so does not cause any significant slowdown first. + node.fs.readdirSync(compilePath).filter: + fileName => fileName.endsWith(".mls") || fileName.endsWith(".mjs") + .toSeq.flatMap: fileName => val filePath = node.path.join(compilePath, fileName) if node.fs.existsSync(filePath) then Some(s"/std/$fileName" -> node.fs.readFileSync(filePath, "utf-8")) else None - .toMap + ("/std/Prelude.mls" -> node.fs.readFileSync(preludePath, "utf-8")) + .toMap + + ("/std/RuntimeJS.mjs" -> node.fs.readFileSync(runtimePath, "utf-8")) + + ("/std/Prelude.mls" -> node.fs.readFileSync(preludePath, "utf-8")) - private val paths = new Paths("/std/Prelude.mls", "/std/Runtime.mjs", "/std/Runtime.mls", "/std/Term.mjs") + private val paths = new Paths("/std/Prelude.mls", "/std/Runtime.mjs", "/std/Runtime.mls", "/std/Term.mjs", "/std") private def createCompiler(): (InMemoryFileSystem, Compiler) = val stdLib = loadStandardLibrary() val fs = new InMemoryFileSystem(stdLib) - given CompilerCtx = CompilerCtx.fresh(fs) + given CompilerCtx = CompilerCtx.fresh(fs, WebModuleResolver()) (fs, new Compiler(paths)) test("compiler can compile a simple program"): val (fs, compiler) = createCompiler() // Write test program to the file system - val code = """|import "./std/Option.mls" - |import "./std/Stack.mls" - |import "./std/Predef.mls" + val code = """|import "/std/Option.mls" + |import "/std/Stack.mls" + |import "/std/Predef.mls" | |open Stack |open Option @@ -57,8 +67,11 @@ class CompilerTest extends AnyFunSuite: val diagnostics = compiler.compile(inputPath) + global.console.log(fs.allFiles.keys.mkString("\n")) + val hasErrors = diagnostics.exists: perFile => val fileDiagnostics = perFile.diagnostics.asInstanceOf[scala.scalajs.js.Array[scala.scalajs.js.Dynamic]] + global.console.log(fileDiagnostics) fileDiagnostics.exists(_.kind is "error") assert(!hasErrors, "Compilation should succeed without errors") diff --git a/hkmc2/js/src/test/support/analyze-smoke.mjs b/hkmc2/js/src/test/support/analyze-smoke.mjs new file mode 100644 index 0000000000..f250c0e000 --- /dev/null +++ b/hkmc2/js/src/test/support/analyze-smoke.mjs @@ -0,0 +1,95 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../../.."); +const modulePath = path.join(repoRoot, "hkmc2/js/target/scala-3.8.3/hkmc2-fastopt/MLscript.mjs"); +const compilePath = path.join(repoRoot, "hkmc2/shared/src/test/mlscript-compile"); +const preludePath = path.join(repoRoot, "hkmc2/shared/src/test/mlscript/decls/Prelude.mls"); + +const { BrowserCompiler, DummyFileSystem, Paths } = await import(path.toNamespacedPath(modulePath)); + +const files = new Map(); +const writes = []; + +for (const fileName of fs.readdirSync(compilePath)) { + if (fileName.endsWith(".mls") || fileName.endsWith(".mjs")) { + files.set(`/std/${fileName}`, fs.readFileSync(path.join(compilePath, fileName), "utf8")); + } +} + +files.set("/std/Prelude.mls", fs.readFileSync(preludePath, "utf8")); +files.set( + "/some-file.mls", + [ + 'import "/std/Stack.mls"', + "", + "module OutlineSmoke with", + " class Box with", + " fun get() = 1", + "", + " object Tools with", + " fun identity(x) = x", + "", + " fun top(x) = x", + "", + ].join("\n"), +); + +const virtualFs = { + read(filePath) { + if (!files.has(filePath)) { + throw new Error(`File not found: ${filePath}`); + } + return files.get(filePath); + }, + write(filePath, content) { + writes.push(filePath); + files.set(filePath, content); + }, + exists(filePath) { + return files.has(filePath); + }, + getLastChangedTimestamp(filePath) { + if (!files.has(filePath)) { + throw new Error(`File not found: ${filePath}`); + } + return 0; + }, +}; + +const compiler = new BrowserCompiler( + new DummyFileSystem(virtualFs), + new Paths("/std/Prelude.mls", "/std/Runtime.mjs", "/std/Term.mjs", "/std"), +); + +const result = compiler.analyze("/some-file.mls"); + +assert.equal(result.schema, "mlscript.symbol-tree"); +assert.equal(result.version, 1); +assert.equal(result.rootFile, "/some-file.mls"); +assert.ok(result.root); +assert.equal(result.root.kind, "file"); +assert.ok(Array.isArray(result.root.children)); +assert.ok(Array.isArray(result.diagnostics)); + +const moduleNode = result.root.children.find((node) => node.name === "OutlineSmoke"); +assert.ok(moduleNode, "expected root module node"); +assert.equal(moduleNode.kind, "module"); + +const boxNode = moduleNode.children.find((node) => node.name === "Box"); +assert.ok(boxNode, "expected nested class node"); +assert.equal(boxNode.kind, "class"); +assert.ok(boxNode.children.some((node) => node.name === "get" && node.kind === "function")); + +const toolsNode = moduleNode.children.find((node) => node.name === "Tools"); +assert.ok(toolsNode, "expected nested object node"); +assert.equal(toolsNode.kind, "object"); +assert.ok(toolsNode.children.some((node) => node.name === "identity" && node.kind === "function")); +assert.ok(moduleNode.children.some((node) => node.name === "top" && node.kind === "function")); + +assert.equal(files.has("/some-file.mjs"), false); +assert.deepEqual(writes.filter((filePath) => filePath.endsWith(".mjs")), []); + +console.log("analyze smoke passed"); diff --git a/hkmc2/jvm/src/main/scala/hkmc2/LocalModuleResolver.scala b/hkmc2/jvm/src/main/scala/hkmc2/LocalModuleResolver.scala new file mode 100644 index 0000000000..1c16386512 --- /dev/null +++ b/hkmc2/jvm/src/main/scala/hkmc2/LocalModuleResolver.scala @@ -0,0 +1,101 @@ + +package hkmc2 + +import hkmc2.utils.*, shorthands.* +import ModuleResolver.* +import io.PlatformPath.{given} +import LocalModuleResolver.Vendor + +/** + * For local tests, including `CompileTestRunner`, `DiffTestRunner`, etc. + * + * @param vendors the list of vendor paths. + * @param nodeModulesPath the optional path to the `node_modules` folder. + * If provided, we will do a simple check to see if + * the requested Node.js built-in module exists. + */ +class LocalModuleResolver(vendors: Ls[Vendor], nodeModulesPath: Opt[io.Path])(using fs: io.FileSystem) extends ModuleResolver: + import LocalModuleResolver.* + + private def existsNodeModule(orgNameOpt: Opt[Str], moduleName: Str): Bool = + nodeModulesPath match + case S(basePath) => + val packagePath = orgNameOpt match + case S(orgName) => basePath / s"@$orgName" / moduleName + case N => basePath / moduleName + fs.exists(packagePath) + case N => false + + protected def tryVerbatim(path: Str): Opt[ResolvedModule.Verbatim] = path match + case r(rawOrgName, modName, rawSubPath) => + val importedName = Opt(rawSubPath) match + case S(subPath) => io.RelPath(subPath).baseName + case N if modName.startsWith("node:") => modName.drop(5) + case N => modName + val exists = Opt(rawOrgName) match + case orgNameOpt @ S(_) => existsNodeModule(orgNameOpt, modName) + case N if modName.startsWith("node:") => + allowedNodeJsModules.contains(modName.drop(5)) + case N => allowedNodeJsModules.contains(modName) || existsNodeModule(N, modName) + if exists then S(ResolvedModule.Verbatim(path, importedName)) else N + case _ => N + + protected def tryFile(rawPath: Str): Opt[ResolvedModule] = + vendors.iterator.map: + case vendor @ Vendor(prefix, path, patterns) if rawPath.startsWith(prefix) => + val relativePath = io.RelPath(rawPath.drop(prefix.length)) + val absolutePath = path / relativePath + if vendor.files.contains(absolutePath) then + S(ResolvedModule.File(absolutePath, absolutePath, relativePath.baseName)) + else + N + case _ => N + .collectFirst: + case S(resolved) => resolved + + def tryResolveModulePath(path: Str): Opt[ResolvedModule] = + ModuleResolver.tryResolveUrl(path) orElse tryVerbatim(path) orElse tryFile(path) + +object LocalModuleResolver: + def apply(stdPath: io.Path, nodeModulesPath: Opt[io.Path] = N)(using fs: io.FileSystem): LocalModuleResolver = + val vendors = Vendor("std/", stdPath, Ls("*.mls", "**/*.mls")) :: Nil + new LocalModuleResolver(vendors, nodeModulesPath) + + import java.nio.file.FileSystems + + case class Vendor(prefix: Str, path: io.Path, patterns: Ls[Str]): + val files: Ls[io.Path] = patterns.iterator.flatMap: pattern => + val m = FileSystems.getDefault.getPathMatcher(s"glob:$pattern") + os.walk(path).iterator.filter: p => + m.matches(path.toNIO.relativize(p.toNIO)) + .map(io.PlatformPath.fromOsPath(_)) + .toList + + println(s"Vendor $prefix: found ${"file" countBy files.length}") + files.foreach: file => + println(s"- $file") + + /** + * The pattern of valid Node.js module specifiers. + * + * 1. **Group 1:** `@([a-z0-9-~][a-z0-9-._~]*)\/` + * + Meaning: Scope (no `@`) + * + Example: `@my-org/foo/bar` β†’ `"my-org"` + * 2. **Group 2:** `(?:node:)?[a-z0-9-~][a-z0-9-._~]*` + * + Meaning: Package name with optional `node:` prefix + * + Example: `@my-org/foo/bar` β†’ `"foo"` + * + Example: `mypkg/test` β†’ `"mypkg"` + * 3. **Group 3:** `\/(.*)` + * + Meaning: The remaining text after the first slash + * + Example: `@my-org/foo/bar/baz` β†’ `"bar/baz"` + * + Example: `mypkg/sub/path` β†’ `"sub/path"` + * + Example: `mypkg` β†’ `null` + */ + private val r = """^(?:@([a-z0-9-~][a-z0-9-._~]*)\/)?((?:node:)?[a-z0-9-~][a-z0-9-._~]*)(?:\/(.*))?$""".r + + /** + * An incomplete list of allowed Node.js built-in modules. Add more as needed. + */ + private val allowedNodeJsModules = Set( + "fs", "path", "http", "https", "url", "util", "events", "stream", "buffer", + "os", "child_process", "vm", "assert", "tty", "process") diff --git a/hkmc2/jvm/src/main/scala/hkmc2/io/PlatformPath.scala b/hkmc2/jvm/src/main/scala/hkmc2/io/PlatformPath.scala index 326732875d..802b5128ce 100644 --- a/hkmc2/jvm/src/main/scala/hkmc2/io/PlatformPath.scala +++ b/hkmc2/jvm/src/main/scala/hkmc2/io/PlatformPath.scala @@ -41,6 +41,10 @@ private[io] class WrappedRelPath(private[io] val underlying: os.RelPath) extends def /(other: RelPath): RelPath = new WrappedRelPath(underlying / other.asInstanceOf[WrappedRelPath].underlying) + + def last: String = underlying.last + + def baseName: String = underlying.baseName /** * Platform-specific factory for creating Path instances @@ -66,3 +70,6 @@ object PlatformPath: /** Implicit conversion from os.RelPath to io.RelPath (import to use) */ given Conversion[os.RelPath, RelPath] = fromOsRelPath + + given getUnderlyingPath: Conversion[io.Path, os.Path] = _ match + case WrappedPath(underlying) => underlying diff --git a/hkmc2/jvm/src/test/scala/hkmc2/CompileTestRunner.scala b/hkmc2/jvm/src/test/scala/hkmc2/CompileTestRunner.scala index 8ae73ae336..301cd12348 100644 --- a/hkmc2/jvm/src/test/scala/hkmc2/CompileTestRunner.scala +++ b/hkmc2/jvm/src/test/scala/hkmc2/CompileTestRunner.scala @@ -15,7 +15,11 @@ end CompileTestRunner object CompileTestRunner: - given cctx: CompilerCtx = CompilerCtx.fresh(io.FileSystem.default) + private val workingDir = os.pwd + private val stdPath = TestFolders.compileTestDir(workingDir) + private val nodeModulesPath = workingDir / "node_modules" + + given cctx: CompilerCtx = + CompilerCtx.fresh(io.FileSystem.default, LocalModuleResolver(stdPath, S(nodeModulesPath))) end CompileTestRunner - diff --git a/hkmc2/jvm/src/test/scala/hkmc2/TestFolders.scala b/hkmc2/jvm/src/test/scala/hkmc2/TestFolders.scala index 3c4c0f708a..89a87aa050 100644 --- a/hkmc2/jvm/src/test/scala/hkmc2/TestFolders.scala +++ b/hkmc2/jvm/src/test/scala/hkmc2/TestFolders.scala @@ -24,6 +24,10 @@ object TestFolders: def compileTestDir(wd: os.Path): os.Path = mainTestDir(wd)/"mlscript-compile" + /** The packages test directory: `hkmc2/shared/src/test/mlscript-packages`. */ + def packagesTestDir(wd: os.Path): os.Path = + mainTestDir(wd)/"mlscript-packages" + // β€”β€”β€” Diff test subdirectories excluded from the main DiffTestRunner β€”β€”β€” /** Diff test subdirectories that belong to the hkmc2NofibTests project. */ @@ -38,9 +42,10 @@ object TestFolders: def wasmDiffDir(wd: os.Path): os.Path = diffTestDir(wd)/"wasm" - /** Diff test directories that are always excluded (staging, mlscript-compile). */ + /** Diff test directories that are always excluded (staging, mlscript-compile, + * mlscript-packages). */ def alwaysExcludedDiffDirs(wd: os.Path): Ls[os.Path] = - (diffTestDir(wd)/"ucs"/"staging") :: compileTestDir(wd) :: Nil + (diffTestDir(wd)/"ucs"/"staging") :: compileTestDir(wd) :: packagesTestDir(wd) :: Nil /** All diff test directories excluded from the main DiffTestRunner. */ def mainExcludedDiffDirs(wd: os.Path): Ls[os.Path] = @@ -66,7 +71,7 @@ object TestFolders: nofibCompileDirs(wd) ::: appsCompileDirs(wd) ::: wasmCompileDirs(wd) /** Compile test directories for the hkmc2NofibTests project. - * We walk from `bench/` so test names include the `mlscript-compile/` prefix. */ + * We walk from `mlscript-compile/nofib/` directly. */ def nofibCompileDirs(wd: os.Path): Ls[os.Path] = compileTestDir(wd)/"nofib" :: Nil @@ -76,9 +81,8 @@ object TestFolders: compileTestDir(wd)/"apps" :: Nil /** Compile test directories for the hkmc2WasmTests project. - * We walk from `mainTestDir` so test names include the `mlscript-compile/` prefix.. */ + * We walk from `mlscript-compile/wasm/` directly. */ def wasmCompileDirs(wd: os.Path): Ls[os.Path] = compileTestDir(wd)/"wasm" :: Nil end TestFolders - diff --git a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala index a58d60a44c..0d6a1528ca 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/CompilerCtx.scala @@ -22,6 +22,7 @@ class CompilerCtx( val importing: Opt[(io.Path, CompilerCtx)], val beingCompiled: Set[io.Path], val fs: io.FileSystem, + val moduleResolver: ModuleResolver, cache: CompilerCache, ): @@ -31,7 +32,7 @@ class CompilerCtx( case N => Nil def derive(newFile: io.Path): CompilerCtx = - CompilerCtx(S(newFile, this), beingCompiled + newFile, fs, cache) + CompilerCtx(S(newFile, this), beingCompiled + newFile, fs, moduleResolver, cache) def getElaboratedBlock (file: io.Path, prelude: Ctx) @@ -88,7 +89,8 @@ object CompilerCtx: inline def get(using cctx: CompilerCtx) = cctx - def fresh(fs: io.FileSystem): CompilerCtx = CompilerCtx(N, Set.empty, fs, new PlatformCompilerCache) + def fresh(fs: io.FileSystem, moduleResolver: io.FileSystem ?=> ModuleResolver): CompilerCtx = + CompilerCtx(N, Set.empty, fs, moduleResolver(using fs), new PlatformCompilerCache) end CompilerCtx diff --git a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala index 22da5a8376..b63c58a911 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/MLsCompiler.scala @@ -9,6 +9,7 @@ import utils.* import hkmc2.semantics.* import hkmc2.syntax.Keyword.`override` import semantics.Elaborator.{Ctx, State} +import hkmc2.io.Path class ParserSetup(file: io.Path, dbgParsing: Bool)(using state: Elaborator.State, raise: Raise, cctx: CompilerCtx): @@ -61,8 +62,14 @@ class MLsCompiler def compileModule(file: io.Path): Unit = + compileModule(file, N) + + /** Compile a MLscript module into JavaScript with the given output path. */ + def compileModule(file: io.Path, outputFile: Opt[io.Path]): Unit = val wd = file.up + val out = outputFile.getOrElse(file.up / io.RelPath(file.baseName + ".mjs")) + val outputWd = out.up given Raise = mkRaise(file) @@ -105,10 +112,10 @@ class MLsCompiler case _ => t.subTerms.exists(findQuote) val hasQuote = findQuote(blk0) val blk = new Term.Blk( - Import(State.runtimeSymbol, runtimeFile.toString, runtimeFile) :: + Import(State.runtimeSymbol, runtimeFile.toString, runtimeFile, ImportKind.Default) :: // Only import `Term.mls` when necessary. (if hasQuote then - Import(State.termSymbol, termFile.toString, termFile) :: blk0.stats + Import(State.termSymbol, termFile.toString, termFile, ImportKind.Default) :: blk0.stats else blk0.stats), blk0.res @@ -134,9 +141,8 @@ class MLsCompiler baseScp.addToBindings(Elaborator.State.importSymbol, "import", shadow = false) val nestedScp = baseScp.nest val je = nestedScp.givenIn: - jsb.program(optimized, exportedSymbol, wd) + jsb.program(optimized, exportedSymbol, outputWd) val jsStr = je.stripBreaks.mkString(100) - val out = file.up / io.RelPath(file.baseName + ".mjs") cctx.fs.write(out, jsStr) } diff --git a/hkmc2/shared/src/main/scala/hkmc2/ModuleResolver.scala b/hkmc2/shared/src/main/scala/hkmc2/ModuleResolver.scala new file mode 100644 index 0000000000..11421ba333 --- /dev/null +++ b/hkmc2/shared/src/main/scala/hkmc2/ModuleResolver.scala @@ -0,0 +1,65 @@ +package hkmc2 + +import hkmc2.utils.*, shorthands.* +import ModuleResolver.* + +trait ModuleResolver: + /** + * Try to resolve path if it refers to a module or a source file in a module. + * + * @param path the import path + * @return the resolved path and module identifier, if any. + */ + def tryResolveModulePath(path: Str): Opt[ResolvedModule] + + /** Try to resolve a module path from a specific source directory. */ + def tryResolveModulePath(path: Str, from: io.Path): Opt[ResolvedModule] = + tryResolveModulePath(path) + + /** + * Return the generated JavaScript target path for a source file when it is + * compiled somewhere other than beside the source. + */ + def targetPathForSource(sourcePath: io.Path): Opt[io.Path] = N + +object ModuleResolver: + def isUrlModuleSpecifier(path: Str): Bool = + path.startsWith("https://") || path.startsWith("http://") + + def urlModuleName(path: Str): Str = + val withoutFragment = path.takeWhile(ch => ch =/= '#' && ch =/= '?').stripSuffix("/") + val lastSegment = withoutFragment.split('/').lastOption.getOrElse("module") + val sanitized = lastSegment.map: + case ch if ch.isLetterOrDigit || ch === '_' || ch === '$' => ch + case _ => '_' + val nonEmpty = if sanitized.isEmpty then "module" else sanitized + if nonEmpty.headOption.exists(_.isDigit) then "_" + nonEmpty else nonEmpty + + def tryResolveUrl(path: Str): Opt[ResolvedModule.Verbatim] = + // For URLs, we just return the path as-is. + if isUrlModuleSpecifier(path) then S(ResolvedModule.Verbatim(path, urlModuleName(path))) + else N + + /** The result of module resolution. */ + enum ResolvedModule: + /** The module's name, to be used as the identifier. */ + val moduleName: Str + + /** + * The module specifier will be used as-is, e.g., built-in modules, or any + * modules that are resolved by the runtime. + * + * @param specifier the module specifier used in the import statement. + * @param moduleName the module's name, to be used as the identifier. + */ + case Verbatim(specifier: Str, moduleName: Str) + + /** + * The module is resolved to a local file. + * + * @param sourcePath the path to the source code, ending with `.mls`. + * @param targetPath the path to the compiled code, used in the import + * statements in the generated JavaScript files. + * @param moduleName the module's name, to be used as the identifier. + */ + case File(sourcePath: io.Path, targetPath: io.Path, moduleName: Str) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala index a991599478..de09a35667 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala @@ -14,6 +14,7 @@ import semantics.* import semantics.Term.* import sem.Elaborator.State +case class ImportSpec(local: ImportSymbol, specifier: Str, kind: ImportKind) /* Important design notes. @@ -35,10 +36,7 @@ must refresh the corresponding symbols – see SymbolRefresher for this purpose. */ -case class Program( - imports: Ls[ImportSymbol -> Str], - main: Block, -) +case class Program(imports: Ls[ImportSpec], main: Block) /** Symbol that can be used in a `SimpleRef`. */ diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockTransformer.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockTransformer.scala index b5c57cd61c..126aaa6912 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockTransformer.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockTransformer.scala @@ -21,10 +21,9 @@ class BlockTransformer(subst: SymbolSubst): def applyMainBlock(main: Block): Block = applyBlock(main) - def applyImport(imp: ImportSymbol -> Str): ImportSymbol -> Str = - val (l, s) = imp - val l2 = applyImportSymbol(l) - if l2 is l then imp else l2 -> s + def applyImport(imp: ImportSpec): ImportSpec = + val l2 = applyImportSymbol(imp.local) + if l2 is imp.local then imp else imp.copy(local = l2) def applySubBlock(b: Block): Block = applyBlock(b) @@ -210,7 +209,7 @@ class BlockTransformer(subst: SymbolSubst): def applyImportSymbol(sym: ImportSymbol): ImportSymbol = sym match case sym: TempSymbol => sym.subst case sym: VarSymbol => sym.subst - case sym: BlockMemberSymbol => sym.subst + case sym: MemberSymbol => sym.subst def applyAssignLhs(sym: Assignable): Assignable = sym match case NoSymbol => NoSymbol diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockTraverser.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockTraverser.scala index 223ced1858..472526a4ed 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockTraverser.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockTraverser.scala @@ -20,8 +20,8 @@ class BlockTraverser: prog.imports.foreach(applyImport) applyBlock(prog.main) - def applyImport(imp: ImportSymbol -> Str): Unit = - applySymbol(imp._1) + def applyImport(imp: ImportSpec): Unit = + applySymbol(imp.local) def applyMaybeSymbol(sym: MaybeSymbol): Unit = sym match diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala index c3a2652577..95e4d867bd 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala @@ -1372,10 +1372,10 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter): case None => desug case Some(dCfg) => flowAnalysis.FlowAnalysis.mkTraceLogger(dCfg.config, "deforest > ", outterTl).givenIn: - deforest.Deforest(Program(imps.map(imp => imp.sym -> imp.str), desug)).main + deforest.Deforest(Program(imps.map(imp => ImportSpec(imp.sym, imp.str, imp.kind)), desug)).main val etaExpanded = - EtaExpansion(Program(imps.map(imp => imp.sym -> imp.str), deforested)).main + EtaExpansion(Program(imps.map(imp => ImportSpec(imp.sym, imp.str, imp.kind)), deforested)).main val lifted = if lift then Lifter(etaExpanded).transform @@ -1402,7 +1402,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter): else staged Program( - imps.map(imp => imp.sym -> imp.str), + imps.map(imp => ImportSpec(imp.sym, imp.str, imp.kind)), res ) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Printer.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Printer.scala index 8efbb7e062..e0dd7ce387 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Printer.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Printer.scala @@ -210,11 +210,11 @@ class Printer(using Raise, ShowCfg, State, SymbolPrinter, Config): } }" case x: Path => print(x) - def print(imports: Ls[ImportSymbol -> Str])(using Scope): Document = - imports.map: (local, path) => - val docLocal = scope.allocateName(local) - doc"""import "..." as ${docLocal}; # """ - .mkDocument() + def print(imports: Ls[ImportSpec])(using Scope): Document = + imports.map: importSpec => + val docLocal = scope.allocateName(importSpec.local) + doc"""import "..." as ${docLocal}; # """ + .mkDocument() def print(prog: Program)(using Scope): Document = doc"${print(prog.imports)}${print(prog.main)}" diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala index b26ecadade..16d4913dc5 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala @@ -843,16 +843,30 @@ class JSBuilder(using Config, TL, State, Ctx) extends CodeBuilder: reserveNames(p) // Allocate names for imported modules. p.imports.foreach: i => - i._1 -> scope.allocateName(i._1) + i.local -> scope.allocateName(i.local) // Generate import statements. val imps = p.imports.map: i => - val path = i._2 + val path = i.specifier val relPath = if path.startsWith("/") then "./" + io.Path(path).relativeTo(wd).map(_.toString).getOrElse(path) else path - doc"""import ${scope.lookup_!(i._1, N)} from "${relPath}";""" + i.kind match + case ImportKind.Default => + doc"""import ${scope.lookup_!(i.local, N)} from "${relPath}";""" + case ImportKind.Namespace => + doc"""import * as ${scope.lookup_!(i.local, N)} from "${relPath}";""" + case ImportKind.Named(importedName) => + doc"""import { ${importedName} as ${scope.lookup_!(i.local, N)} } from "${relPath}";""" + // A module's top-level Block cannot use a JS `return` statement. + // Lowering's `program` tail-op (ImplctRet) wraps the final expression in + // `Return`, which is correct for the worksheet path (the diff-test maps + // Return into an assignment so the result can be displayed) but illegal + // when we emit a module. Convert any top-level Return here to a plain + // statement before handing the block to `block`. + val main = p.main.mapReturn: + case Return(res) => Assign(State.noSymbol, res, End()) withPrivateAccessorDecls(imps.mkDocument(doc" # ")) - :/: nonNestedScoped(p.main)(block(_, endSemi = false)).stripBreaks + :/: nonNestedScoped(main)(block(_, endSemi = false)).stripBreaks :: locally: exprt match case S(sym) => @@ -863,17 +877,22 @@ class JSBuilder(using Config, TL, State, Ctx) extends CodeBuilder: collectExternalPrivateAccessors(p) reserveNames(p) lazy val imps = p.imports.map: i => - doc"""${scope.lookup_!(i._1, N)} = await import("${i._2.toString}").then(m => m.default ?? m);""" + val importPrefix = doc"""${scope.lookup_!(i.local, N)} = await import("${i.specifier}")""" + i.kind match + case ImportKind.Default => importPrefix :: doc".then(m => m.default ?? m);" + case ImportKind.Namespace => importPrefix :: doc";" + case ImportKind.Named(importedName) => + importPrefix :: doc".then(m => m[${makeStringLiteral(importedName)}]);" p.main match case Scoped(syms, body) => val fvs = body.freeVars - blockPreamble(p.imports.map(_._1) ++ syms.view.filter(s => + blockPreamble(p.imports.map(_.local) ++ syms.view.filter(s => !s.isInstanceOf[TempSymbol] // ^ VarSymbols and TermSymbols should be kept as their value will be acessed and printed by the worksheet || fvs(s))) -> (withPrivateAccessorDecls(imps.mkDocument(doc" # ")) :/: block(body, endSemi = false).stripBreaks) case body => - blockPreamble(p.imports.map(_._1)) -> + blockPreamble(p.imports.map(_.local)) -> (withPrivateAccessorDecls(imps.mkDocument(doc" # ")) :/: returningTerm(body, endSemi = false).stripBreaks) def genLetDecls(vars: Iterator[(Symbol, Str)]): Document = 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 0904a24735..8547c23ed2 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 @@ -2238,7 +2238,7 @@ class WatBuilder(using TraceLogger, State) extends CodeBuilder: for imprt <- p.imports do raise( ErrorReport( - msg"Import of symbol `${imprt._2}` not implemented yet" -> imprt._1.toLoc :: Nil, + msg"Import of symbol `${imprt.specifier}` not implemented yet" -> imprt.local.toLoc :: Nil, extraInfo = S(imprt), source = Diagnostic.Source.Compilation, ), diff --git a/hkmc2/shared/src/main/scala/hkmc2/invalml/InvalML.scala b/hkmc2/shared/src/main/scala/hkmc2/invalml/InvalML.scala index 18261d36fa..ff07f0ce48 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/invalml/InvalML.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/invalml/InvalML.scala @@ -653,7 +653,7 @@ class InvalTyper(using elState: Elaborator.State, tl: TL)(using Ctx): case (modDef: ModuleOrObjectDef) :: stats => typeNames.add(modDef.sym.nme) goStats(stats) - case Import(sym, str, pth) :: stats => + case Import(sym, str, pth, kind) :: stats => goStats(stats) // TODO: case stat :: _ => TODO(stat) diff --git a/hkmc2/shared/src/main/scala/hkmc2/io/Path.scala b/hkmc2/shared/src/main/scala/hkmc2/io/Path.scala index a582392afd..c821b45c2a 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/io/Path.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/io/Path.scala @@ -52,6 +52,7 @@ abstract class RelPath: def toString: String def segments: Ls[String] def /(other: RelPath): RelPath + def baseName: String object RelPath: /** Create relative path from string - delegates to platform-specific implementation */ diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala index 74b63abcbe..55acfe9e09 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala @@ -1687,25 +1687,46 @@ extends Importer: go(sts, Nil, acc) case (m @ PrefixApp(Keywrd(Keyword.`import`), arg)) :: sts => reportUnusedAnnotations - val pathAndAlias: Opt[(Tree, Opt[Ident])] = arg match - case InfixApp(pathArg, Keywrd(Keyword.`as`), alias: Ident) => S((pathArg, S(alias))) - case InfixApp(pathArg, Keywrd(Keyword.`as`), Error()) => N + def importMember(tree: Tree): Opt[ImportSelection] = tree match + case id: Ident => S(ImportSelection.Named(id, N)) + case InfixApp(imported: Ident, Keywrd(Keyword.`as`), alias: Ident) => + S(ImportSelection.Named(imported, S(alias))) + case InfixApp(imported: Ident, Keywrd(Keyword.`as`), Error()) => N case InfixApp(_, Keywrd(Keyword.`as`), badAlias) => raise(ErrorReport( msg"Expected identifier after 'as' in import statement" -> badAlias.toLoc :: Nil)) N - case pathArg => S((pathArg, N)) - val (newCtx, newAcc) = pathAndAlias match - case S((StrLit(path), alias)) => - val stmt = importPath(path, alias).withLocOf(m) - (ctx + (stmt.sym.nme -> stmt.sym), - stmt :: acc) - case S((pathArg, _)) => + case badMember => raise(ErrorReport( - msg"Expected string literal after 'import' keyword" -> - pathArg.toLoc :: Nil)) - (ctx, acc) + msg"Expected identifier in import member list" -> + badMember.toLoc :: Nil)) + N + val parsedImports: Opt[Ls[(Tree, ImportSelection)]] = arg match + case Jux(pathArg, Block(members)) => + S(members.flatMap: member => + importMember(member).map(pathArg -> _).toList) + case InfixApp(pathArg, Keywrd(Keyword.`as`), alias: Ident) => + S((pathArg, ImportSelection.Namespace(alias)) :: Nil) + case InfixApp(pathArg, Keywrd(Keyword.`as`), Error()) => N + case InfixApp(_, Keywrd(Keyword.`as`), badAlias) => + raise(ErrorReport( + msg"Expected identifier after 'as' in import statement" -> + badAlias.toLoc :: Nil)) + N + case pathArg => S((pathArg, ImportSelection.Default(N)) :: Nil) + val (newCtx, newAcc) = parsedImports match + case S(imports) => + imports.foldLeft(ctx -> acc): + case ((nextCtx, nextAcc), (path: StrLit, selection)) => + val stmt = importPath(path, selection).withLocOf(m) + (nextCtx + (stmt.sym.nme -> stmt.sym), + stmt :: nextAcc) + case ((nextCtx, nextAcc), (pathArg, _)) => + raise(ErrorReport( + msg"Expected string literal after 'import' keyword" -> + pathArg.toLoc :: Nil)) + (nextCtx, nextAcc) case N => // errors have been reported above. (ctx, acc) newCtx.givenIn: diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala index e987655c66..3f26d0d71d 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala @@ -11,65 +11,105 @@ import hkmc2.io import utils.TraceLogger import Elaborator.* -import hkmc2.syntax.LetBind +import hkmc2.syntax.Tree, Tree.{Ident, StrLit} +enum ImportSelection: + case Default(alias: Opt[Ident]) + case Namespace(alias: Ident) + case Named(imported: Ident, alias: Opt[Ident]) class Importer: self: Elaborator => import tl.* + import ImportKind.{Default as DefaultImport, Namespace as NamespaceImport, Named as NamedImport} + import ImportSelection.{Default as DefaultSelection, Namespace as NamespaceSelection, Named as NamedSelection} - def importPath(path: Str, alias: Opt[syntax.Tree.Ident])(using cfg: Config): Import = - // log(s"pwd: ${os.pwd}") - // log(s"wd: ${wd}") - - val file = - if path.startsWith("/") - then io.Path(path) - else wd / io.RelPath(path) - - val nme = file.baseName - val id = alias.getOrElse(new syntax.Tree.Ident(nme)) // TODO loc + def importPath(rawPath: StrLit, selection: ImportSelection)(using cfg: Config): Import = + cctx.moduleResolver.tryResolveModulePath(rawPath.value, wd) match + case S(ModuleResolver.ResolvedModule.Verbatim(specifier, moduleName)) => + // The path resolves to a platform dependent specifier, which is NOT a + // path and should be used as-is, e.g., Node.js built-in modules. + val id = localId(selection, moduleName) + val sym = VarSymbol(id) + Import(sym, specifier, wd / io.RelPath(moduleName), importKind(selection, moduleName)) + case S(ModuleResolver.ResolvedModule.File(sourceFile, targetFile, moduleName)) => + // The specifier is resolved to a file path. + importFile(rawPath, sourceFile, targetFile, moduleName, selection) + case N => + // The specifier could not be resolved. We treat it as a file path. + val actualFile = + if rawPath.value.startsWith("/") then io.Path(rawPath.value) + else wd / io.RelPath(rawPath.value) + val targetFile = cctx.moduleResolver.targetPathForSource(actualFile).getOrElse(actualFile) + importFile(rawPath, actualFile, targetFile, actualFile.baseName, selection) + + private def localId(selection: ImportSelection, moduleName: Str): Ident = selection match + case DefaultSelection(alias) => alias.getOrElse(new Ident(moduleName)) // TODO loc + case NamespaceSelection(alias) => alias + case NamedSelection(imported, alias) => alias.getOrElse(imported) + + private def importKind(selection: ImportSelection, moduleName: Str): ImportKind = selection match + case DefaultSelection(_) => DefaultImport + case NamespaceSelection(_) => NamespaceImport + case NamedSelection(imported, _) => + if imported.name === moduleName then DefaultImport else NamedImport(imported.name) + + private def importFile(rawPath: StrLit, actualFile: io.Path, targetFile: io.Path, nme: Str, selection: ImportSelection)(using cfg: Config): Import = + val id = localId(selection, nme) + val kind = importKind(selection, nme) lazy val sym = VarSymbol(id) - if path.startsWith(".") || path.startsWith("/") then // leave alone imports like "fs" - log(s"importing $file") - - val nme = file.baseName - val id = new syntax.Tree.Ident(nme) // TODO loc + log(s"importing $actualFile") + + if cctx.fs.exists(actualFile) then - file.ext match + actualFile.ext match case "mjs" | "js" => - Import(sym, file.toString, file) + Import(sym, targetFile.toString, targetFile, kind) case "mls" if { - !cctx.beingCompiled.contains(file) `||`: + !cctx.beingCompiled.contains(actualFile) `||`: raise: ErrorReport: msg"Circular imports of `mls` files are not yet supported" -> N - :: (cctx.allFilesBeingImported :+ file).map(f => msg" importing ${f.toString}" -> N) + :: (cctx.allFilesBeingImported :+ actualFile).map(f => msg" importing ${f.toString}" -> N) false } => - val importedSym = tl.trace(s">>> Importing $file"): + val importedSym: ImportSymbol = tl.trace(s">>> Importing $actualFile"): given TL = tl - val artifact = cctx.getElaboratedBlock(file, prelude) - 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") - val sym: VarSymbol | BlockMemberSymbol = alias.fold(importedSym): alias => - VarSymbol(alias) + val artifact = cctx.getElaboratedBlock(actualFile, prelude) + kind match + case DefaultImport => + artifact.tree.definedSymbols.find(_._1 === nme) match + case Some(nme -> imsym) => imsym + case None => lastWords(s"File $actualFile does not define a symbol named $nme") + case NamespaceImport => + sym + case NamedImport(importedName) => + raise: + ErrorReport( + msg"Named imports from MLscript sources currently support only the default module name '$nme'" -> + rawPath.toLoc :: Nil) + sym + val selectedSym = (selection, importedSym) match + case (DefaultSelection(S(alias)), _) => VarSymbol(alias) + case (NamedSelection(_, S(alias)), _) if kind === DefaultImport => VarSymbol(alias) + case _ => importedSym - val jsFile = file.up / io.RelPath(file.baseName + ".mjs") - Import(sym, jsFile.toString, jsFile) + val jsFile = + if targetFile.ext === "mjs" then targetFile + else targetFile.up / io.RelPath(targetFile.baseName + ".mjs") + Import(selectedSym, jsFile.toString, jsFile, kind) case _ => - if file.ext =/= "mls" then raise: - ErrorReport(msg"Unsupported file extension: ${file.ext}" -> N :: Nil) - Import(sym, path, file) + if actualFile.ext =/= "mls" then raise: + ErrorReport(msg"Unsupported file type" -> rawPath.toLoc :: Nil) + Import(sym, rawPath.value, actualFile, kind) else - Import(sym, path, file) - - + raise: + ErrorReport(msg"Cannot resolve the import path ${actualFile.toString}" -> rawPath.toLoc :: Nil) + Import(sym, rawPath.value, actualFile, kind) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala index b5cee7f206..4a80ea5648 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala @@ -165,7 +165,7 @@ type NoSymbol = NoSymbol.type * User-facing imports bind variable or member symbols, while compiler-generated imports * such as prelude/runtime imports may bind temporary term values directly. */ -type ImportSymbol = TempSymbol | VarSymbol | BlockMemberSymbol +type ImportSymbol = TempSymbol | VarSymbol | MemberSymbol abstract class FlowSymbol(label: Str)(using State) extends Symbol: diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala index 8f8fa55b79..2840252d0e 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala @@ -628,7 +628,7 @@ sealed trait Statement extends AutoLocated, ProductWithExtraInfo: def mkClone(using State): Statement = this match case t: Term => lastWords(s"overridden implementation") case d: Definition => ??? - case imp: Import => Import(imp.sym, imp.str, imp.file) + case imp: Import => Import(imp.sym, imp.str, imp.file, imp.kind) case LetDecl(sym, annotations) => LetDecl(sym, annotations.map(_.mkClone)) case RcdField(field, rhs) => RcdField(field.mkClone, rhs.mkClone) case RcdSpread(rcd) => RcdSpread(rcd.mkClone) @@ -753,7 +753,7 @@ sealed trait Statement extends AutoLocated, ProductWithExtraInfo: td.rhs.toVector ++ td.annotations.flatMap(_.subTerms).toVector case pat: PatternDef => (pat.paramsOpt.toVector.flatMap(_.subTerms) :+ pat.body.blk) ++ pat.annotations.flatMap(_.subTerms).toVector - case Import(sym, str, pth) => Vector.empty + case Import(sym, str, pth, kind) => Vector.empty case Try(body, finallyDo) => Vector.single(body) ++ Vector.single(finallyDo) case Handle(lhs, rhs, args, derivedClsSym, defs, bod) => (rhs +: args.toVector) ++ defs.flatMap(_.td.subTerms).toVector :+ bod case Neg(e) => Vector.single(e) @@ -946,7 +946,7 @@ sealed trait Statement extends AutoLocated, ProductWithExtraInfo: s"${cls.kind} ${cls.sym.nme}${ cls.tparams.map(_.showDbg).mkStringOr(", ", "[", "]")}${ cls.paramsOpt.fold("")(_.toString)} ${cls.body}" - case Import(sym, str, file) => s"import $str from ${file}" + case Import(sym, str, file, kind) => s"import $str from ${file}" case Annotated(ann, target) => s"@${ann} ${target.showDbg}" case Throw(res) => s"throw ${res.showDbg}" case Label(label, _, body, _) => s"do ${label.nme}: ${body.showDbg}" @@ -1112,13 +1112,18 @@ case class ObjBody(blk: Term.Blk): end ObjBody -/** `sym` is a `BlockMemberSymbol` or a `VarSymbol` when the import is made by the user - * and can be referred to by name (it's either the BMS of the imported module +enum ImportKind: + case Default + case Namespace + case Named(importedName: Str) + +/** `sym` is a `MemberSymbol` or a `VarSymbol` when the import is made by the user + * and can be referred to by name (it's either the MemberSymbol of the imported module * or the VarSymbol of the alias, in an aliased import `import "..." as alias`), * in which case it is a `BlockMemberSymbol` when importing files explicitly * and a `TermSymbol` when the import is made implicitly by the compiler (eg, importing "Predef"). * Note that the `file` Path may not represent a real file; eg when importing "fs". */ -case class Import(sym: ImportSymbol, str: Str, file: io.Path) extends Statement +case class Import(sym: ImportSymbol, str: Str, file: io.Path, kind: ImportKind) extends Statement sealed abstract class Declaration: diff --git a/hkmc2/shared/src/main/scala/hkmc2/syntax/Parser.scala b/hkmc2/shared/src/main/scala/hkmc2/syntax/Parser.scala index 15e49212b2..cfee3b3792 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/syntax/Parser.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/syntax/Parser.scala @@ -158,28 +158,37 @@ abstract class Parser( protected var indent = 0 private var _cur: Ls[TokLoc] = preprocessTokens(tokens) - private def preprocessTokens(tokens: Ls[TokLoc]): Ls[TokLoc] = tokens match - case (IDENT("new", false), l1) :: (IDENT("!", true), l2) :: rest => - (IDENT("new!", false), l1 ++ l2) :: preprocessTokens(rest) - // * Remove empty indented sections - case (BRACKETS(Indent, toks), _) :: rest - if toks.forall: - case (NEWLINE | SPACE, _) => true - case _ => false - => - preprocessTokens(rest) - // * Expands end-of-line suspensions that introduce implied indentation, - // * skipping NOISE tokens between `...` and NEWLINE (eg `... // hello\n body`) - // * Note: using `NEWLINE_COMMA` instead of `NEWLINE` causes misparsing of things like `fun foo(..., ...)` - case (SUSPENSION(true), l0) :: NOISE((NEWLINE, l1) :: rest) => - val outerLoc = l0.left ++ rest.lastOption.map(_._2.right) - val innerLoc = l1.right ++ rest.lastOption.map(_._2.left) - BRACKETS(Indent, preprocessTokens(rest))(innerLoc) -> outerLoc :: Nil - case tl :: rest => - val rest2 = preprocessTokens(rest) - if rest2 is rest then tokens - else tl :: rest2 - case Nil => tokens + // Keep the top-level scan iterative: Scala.js can overflow the browser stack + // when recursively preprocessing large standard-library token streams. + private def preprocessTokens(tokens: Ls[TokLoc]): Ls[TokLoc] = + val out = List.newBuilder[TokLoc] + var cur = tokens + var done = false + while !done do cur match + case (IDENT("new", false), l1) :: (IDENT("!", true), l2) :: rest => + out += ((IDENT("new!", false), l1 ++ l2)) + cur = rest + // * Remove empty indented sections + case (BRACKETS(Indent, toks), _) :: rest + if toks.forall: + case (NEWLINE | SPACE, _) => true + case _ => false + => + cur = rest + // * Expands end-of-line suspensions that introduce implied indentation, + // * skipping NOISE tokens between `...` and NEWLINE (eg `... // hello\n body`) + // * Note: using `NEWLINE_COMMA` instead of `NEWLINE` causes misparsing of things like `fun foo(..., ...)` + case (SUSPENSION(true), l0) :: NOISE((NEWLINE, l1) :: rest) => + val outerLoc = l0.left ++ rest.lastOption.map(_._2.right) + val innerLoc = l1.right ++ rest.lastOption.map(_._2.left) + out += (BRACKETS(Indent, preprocessTokens(rest))(innerLoc) -> outerLoc) + done = true + case tl :: rest => + out += tl + cur = rest + case Nil => + done = true + out.result() private def wrap[R](args: => Any)(using l: Line, n: Name)(mkRes: => R): R = printDbg(s"@ ${n.value}${args match { diff --git a/hkmc2/shared/src/test/mlscript-compile/Predef.mjs b/hkmc2/shared/src/test/mlscript-compile/Predef.mjs index b15c43a2e8..604a2cd301 100644 --- a/hkmc2/shared/src/test/mlscript-compile/Predef.mjs +++ b/hkmc2/shared/src/test/mlscript-compile/Predef.mjs @@ -4,7 +4,6 @@ import runtime from "./Runtime.mjs"; import RuntimeJS from "./RuntimeJS.mjs"; import Runtime from "./Runtime.mjs"; import Rendering from "./Rendering.mjs"; -import Term from "./Term.mjs"; let Predef1, lambda, lambda1, lambda$, lambda$1, lambda$2; lambda$2 = (undefined, function (Predef2) { return (acc, x) => { @@ -111,19 +110,6 @@ lambda = (undefined, function (Predef2, a, b, field) { Predef.render = Rendering.render; Predef.js_assert = globalThis.console["assert"]; Predef.foldl = Predef.fold; - (class meta { - static { - Predef.meta = this - } - static codegen(t, file) { - return runtime.safeCall(Term.codegen(t, file)) - } - static print(t) { - return runtime.safeCall(Term.print(t)) - } - toString() { return runtime.render(this); } - static [definitionMetadata] = ["class", "meta"]; - }); } static id(x) { return x diff --git a/hkmc2/shared/src/test/mlscript-compile/Predef.mls b/hkmc2/shared/src/test/mlscript-compile/Predef.mls index 3658741633..ab9047432f 100644 --- a/hkmc2/shared/src/test/mlscript-compile/Predef.mls +++ b/hkmc2/shared/src/test/mlscript-compile/Predef.mls @@ -1,11 +1,11 @@ import "./RuntimeJS.mjs" -// * Importing the mjs for now to avoid reparsing/re-elaborating these files Runtime all the time +// * Importing Runtime.mjs for now to avoid reparsing/re-elaborating Runtime all the time // * TODO: proper caching of parsed/elaborated files import "./Runtime.mjs" -import "./Rendering.mjs" -import "./Term.mjs" +import "./Rendering.mls" +// import "./Term.mjs" // import "./Runtime.mls" // import "./Rendering.mls" @@ -141,6 +141,6 @@ fun raiseUnhandledEffect() = Runtime.mkEffect(Runtime.FatalEffect, null) -module meta with - fun codegen(t, file) = Term.codegen(t, file) - fun print(t) = Term.print(t) +// module meta with +// fun codegen(t, file) = Term.codegen(t, file) +// fun print(t) = Term.print(t) diff --git a/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs b/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs index eb9f1fbaf2..9623717a87 100644 --- a/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs +++ b/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs @@ -5,55 +5,55 @@ import RuntimeJS from "./RuntimeJS.mjs"; import Rendering from "./Rendering.mjs"; import LazyArray from "./LazyArray.mjs"; import Iter from "./Iter.mjs"; -let Runtime1, lambda, lambda1, lambda2, lambda$, lambda$1, Capture$scope301, lambda$2, Capture$scope321, lambda$3; -(class Capture$scope32 { +let Runtime1, lambda, lambda1, lambda2, lambda$, lambda$1, Capture$scope291, lambda$2, Capture$scope311, lambda$3; +(class Capture$scope31 { static { - Capture$scope321 = this + Capture$scope311 = this } constructor(result$0) { this.result$0 = result$0; } toString() { return runtime.render(this); } - static [definitionMetadata] = ["class", "Capture$scope32"]; + static [definitionMetadata] = ["class", "Capture$scope31"]; }); -lambda$3 = (undefined, function (scope32$cap, cont) { +lambda$3 = (undefined, function (scope31$cap, cont) { return (m, marker) => { - return lambda2(scope32$cap, cont, m, marker) + return lambda2(scope31$cap, cont, m, marker) } }); -lambda2 = (undefined, function (scope32$cap, cont, m, marker) { +lambda2 = (undefined, function (scope31$cap, cont, m, marker) { let scrut, tmp, tmp1; scrut = runtime.safeCall(m.has(cont)); if (scrut === true) { tmp = ", " + marker; - tmp1 = scope32$cap.result$0 + tmp; - scope32$cap.result$0 = tmp1; + tmp1 = scope31$cap.result$0 + tmp; + scope31$cap.result$0 = tmp1; return runtime.Unit } return runtime.Unit; }); -(class Capture$scope30 { +(class Capture$scope29 { static { - Capture$scope301 = this + Capture$scope291 = this } constructor(result$0) { this.result$0 = result$0; } toString() { return runtime.render(this); } - static [definitionMetadata] = ["class", "Capture$scope30"]; + static [definitionMetadata] = ["class", "Capture$scope29"]; }); -lambda$2 = (undefined, function (scope30$cap, cont) { +lambda$2 = (undefined, function (scope29$cap, cont) { return (m, marker) => { - return lambda1(scope30$cap, cont, m, marker) + return lambda1(scope29$cap, cont, m, marker) } }); -lambda1 = (undefined, function (scope30$cap, cont, m, marker) { +lambda1 = (undefined, function (scope29$cap, cont, m, marker) { let scrut, tmp, tmp1; scrut = runtime.safeCall(m.has(cont)); if (scrut === true) { tmp = ", " + marker; - tmp1 = scope30$cap.result$0 + tmp; - scope30$cap.result$0 = tmp1; + tmp1 = scope29$cap.result$0 + tmp; + scope29$cap.result$0 = tmp1; return runtime.Unit } return runtime.Unit; @@ -61,7 +61,7 @@ lambda1 = (undefined, function (scope30$cap, cont, m, marker) { lambda = (undefined, function (l) { let tmp, tmp1; tmp = l.localName + "="; - tmp1 = runtime.safeCall(Rendering.render(l.value)); + tmp1 = Rendering.render(l.value); return tmp + tmp1 }); lambda$1 = (undefined, function (Runtime2) { @@ -223,12 +223,10 @@ lambda$ = (undefined, function (Runtime2, EffectHandle1, value) { return runtime.safeCall(xs.slice(i, tmp)) } static lazySlice(xs, i, j) { - let callPrefix; - callPrefix = runtime.safeCall(LazyArray.dropLeftRight(i, j)); - return runtime.safeCall(callPrefix(xs)) + return LazyArray.dropLeftRight(i, j)(xs) } static lazyConcat(...args) { - return runtime.safeCall(LazyArray.__concat(...args)) + return LazyArray.__concat(...args) } static get(xs, i) { let scrut, scrut1, tmp; @@ -244,7 +242,7 @@ lambda$ = (undefined, function (Runtime2, EffectHandle1, value) { return xs.at(i); } static isArrayLike(xs) { - return runtime.safeCall(Iter.isArrayLike(xs)) + return Iter.isArrayLike(xs) } toString() { return runtime.render(this); } static [definitionMetadata] = ["class", "Tuple"]; @@ -916,12 +914,12 @@ lambda$ = (undefined, function (Runtime2, EffectHandle1, value) { return header; } static showFunctionContChain(cont, hl, vis, reps) { - let scrut, scrut1, scrut2, tmp, tmp1, tmp2, tmp3, tmp4, scope30$cap, lambda$here; - scope30$cap = new Capture$scope301(undefined); + let scrut, scrut1, scrut2, tmp, tmp1, tmp2, tmp3, tmp4, scope29$cap, lambda$here; + scope29$cap = new Capture$scope291(undefined); if (cont instanceof Runtime.FunctionContFrame.class) { tmp = cont.constructor.name + "(pc="; - scope30$cap.result$0 = tmp + cont.saved.at(1); - lambda$here = lambda$2(scope30$cap, cont); + scope29$cap.result$0 = tmp + cont.saved.at(1); + lambda$here = lambda$2(scope29$cap, cont); runtime.safeCall(hl.forEach(lambda$here)); scrut = runtime.safeCall(vis.has(cont)); if (scrut === true) { @@ -931,12 +929,12 @@ lambda$ = (undefined, function (Runtime2, EffectHandle1, value) { if (scrut1 === true) { throw runtime.safeCall(globalThis.Error("10 repeated continuation frame (loop?)")) } - tmp2 = scope30$cap.result$0 + ", REPEAT"; - scope30$cap.result$0 = tmp2; + tmp2 = scope29$cap.result$0 + ", REPEAT"; + scope29$cap.result$0 = tmp2; } else { runtime.safeCall(vis.add(cont)); } - tmp3 = scope30$cap.result$0 + ") -> "; + tmp3 = scope29$cap.result$0 + ") -> "; tmp4 = Runtime.showFunctionContChain(cont.next, hl, vis, reps); return tmp3 + tmp4 } @@ -947,11 +945,11 @@ lambda$ = (undefined, function (Runtime2, EffectHandle1, value) { return "(NOT CONT)"; } static showHandlerContChain(cont, hl, vis, reps) { - let scrut, scrut1, scrut2, tmp, tmp1, tmp2, tmp3, scope32$cap, lambda$here; - scope32$cap = new Capture$scope321(undefined); + let scrut, scrut1, scrut2, tmp, tmp1, tmp2, tmp3, scope31$cap, lambda$here; + scope31$cap = new Capture$scope311(undefined); if (cont instanceof Runtime.HandlerContFrame.class) { - scope32$cap.result$0 = cont.handler.constructor.name; - lambda$here = lambda$3(scope32$cap, cont); + scope31$cap.result$0 = cont.handler.constructor.name; + lambda$here = lambda$3(scope31$cap, cont); runtime.safeCall(hl.forEach(lambda$here)); scrut = runtime.safeCall(vis.has(cont)); if (scrut === true) { @@ -961,12 +959,12 @@ lambda$ = (undefined, function (Runtime2, EffectHandle1, value) { if (scrut1 === true) { throw runtime.safeCall(globalThis.Error("10 repeated continuation frame (loop?)")) } - tmp1 = scope32$cap.result$0 + ", REPEAT"; - scope32$cap.result$0 = tmp1; + tmp1 = scope31$cap.result$0 + ", REPEAT"; + scope31$cap.result$0 = tmp1; } else { runtime.safeCall(vis.add(cont)); } - tmp2 = scope32$cap.result$0 + " -> "; + tmp2 = scope31$cap.result$0 + " -> "; tmp3 = Runtime.showFunctionContChain(cont.next, hl, vis, reps); return tmp2 + tmp3 } diff --git a/hkmc2/shared/src/test/mlscript-compile/Runtime.mls b/hkmc2/shared/src/test/mlscript-compile/Runtime.mls index 7ca96f1e33..a7323da1af 100644 --- a/hkmc2/shared/src/test/mlscript-compile/Runtime.mls +++ b/hkmc2/shared/src/test/mlscript-compile/Runtime.mls @@ -1,7 +1,7 @@ import "./RuntimeJS.mjs" -import "./Rendering.mjs" -import "./LazyArray.mjs" -import "./Iter.mjs" +import "./Rendering.mls" +import "./LazyArray.mls" +import "./Iter.mls" open annotations { mayNotRaiseEffects } diff --git a/hkmc2/shared/src/test/mlscript-compile/RuntimeJS.mjs b/hkmc2/shared/src/test/mlscript-compile/RuntimeJS.mjs index ce12c69ce6..c220ff072d 100644 --- a/hkmc2/shared/src/test/mlscript-compile/RuntimeJS.mjs +++ b/hkmc2/shared/src/test/mlscript-compile/RuntimeJS.mjs @@ -15,6 +15,10 @@ const RuntimeJS = { try { return computation() } catch (error) { return onError(error) } }, + try_finally(computation, onFinally) { + try { return computation() } + finally { return onFinally() } + }, symbols: { definitionMetadata: Symbol.for("mlscript.definitionMetadata"), prettyPrint: Symbol.for("mlscript.prettyPrint") diff --git a/hkmc2/shared/src/test/mlscript-compile/apps/parsing/PrattParsing.mls b/hkmc2/shared/src/test/mlscript-compile/apps/parsing/PrattParsing.mls index aab7edb932..264ba42ede 100644 --- a/hkmc2/shared/src/test/mlscript-compile/apps/parsing/PrattParsing.mls +++ b/hkmc2/shared/src/test/mlscript-compile/apps/parsing/PrattParsing.mls @@ -1,8 +1,8 @@ -import "../../Predef.mls" -import "../../Stack.mls" -import "../../Option.mls" -import "../../Iter.mls" -import "../../StrOps.mls" +import "std/Predef.mls" +import "std/Stack.mls" +import "std/Option.mls" +import "std/Iter.mls" +import "std/StrOps.mls" import "./Lexer.mls" import "./Token.mls" import "./Expr.mls" diff --git a/hkmc2/shared/src/test/mlscript-packages/.gitignore b/hkmc2/shared/src/test/mlscript-packages/.gitignore new file mode 100644 index 0000000000..05cb4eaac6 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/.gitignore @@ -0,0 +1,3 @@ +*.mjs +vendors/ +!generalized-pratt-parsing/thirdparty/railroad/railroad.mjs diff --git a/hkmc2/shared/src/test/mlscript-packages/README.md b/hkmc2/shared/src/test/mlscript-packages/README.md new file mode 100644 index 0000000000..3f7dad7032 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/README.md @@ -0,0 +1,79 @@ +# MLscript Packages + +Each direct child directory is treated as one package. For example, +`mlscript-packages/web-ide` defines the package named `web-ide`. + +The first package-resolution goal is small: packages can import +outside source trees through explicit vendoring, while relative imports inside +a package keep their current behavior. + +## Package Manifest File + +Each package's directory should contain a manifest file `manifest.json`. +The file will be written in `.mlson` (_MLscript Object Notation_) in the future. + +Minimal shape: + +```json +{ + "name": "recursive-descent-parsing", + "main": "RecursiveDescent.mls", + "moduleName": "RecursiveDescent", + "vendors": [ + { + "prefix": "std/", + "path": "../../mlscript-compile", + "files": ["Predef.mls", "Stack.mls", "Option.mls"] + }, + { + "prefix": "gpp/", + "path": "../generalized-pratt-parsing", + "files": ["Token.mls"] + } + ] +} +``` + +Required fields: + +- `name`: package name. This should match the package directory name. +- `main`: entry `.mls` file for `import "package-name"`. +- `moduleName`: MLscript symbol expected to be defined by the entry file. + +Optional fields: + +- `vendors`: external source roots made available to this package. + +## Vendoring + +`vendors` is the package interdependency mechanism. It is intentionally not an +npm-style dependency graph: a package explicitly chooses which source roots and +which entry files it vendors. + +Each vendor entry has: +- `prefix`: import prefix visible to this package. +- `path`: source root, relative to the current package directory. +- `files`: initial `.mls` files or glob patterns under `path`. + +For each vendor entry, the package compiler: +1. starts from the `.mls` files matched by `files`; +2. recursively follows their `.mls` imports; +3. compiles the transitive `.mls` closure into `.mjs` files under + `vendors//` in the current package; +4. copies all `.js`/`.mjs` files under the declared vendor roots as static + assets, without analyzing their imports; +5. leaves the generated `.mjs` files untracked by version control. + +If a JavaScript asset would be copied to the same path as a compiled `.mls` +output, the compiled `.mls` output wins. + +The compiler injects `Runtime.mjs` into every +generated JavaScript module. +We place this file at `vendors/std/` of each package. + +When vendored code imports another package, package tests read that package's +manifest too. Each package is copied into the current package's +`vendors/` directory at most once, and all generated imports point to that same +copy. + +The package's own `.mls` files still compile beside themselves. diff --git a/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/Extension.mls b/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/Extension.mls new file mode 100644 index 0000000000..b2003b6700 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/Extension.mls @@ -0,0 +1,111 @@ +import "./Keywords.mls" +import "./Rules.mls" +import "./Token.mls" +import "./ParseRule.mls" +import "./Tree.mls" +import "std/Stack.mls" +import "std/Option.mls" +import "std/Predef.mls" +import "std/Iter.mls" +import "std/MutMap.mls" + +open Predef +open Stack +open Option { Some, None } +open Token { LiteralKind } +open ParseRule { Choice } + +module Extension with ... + +fun isDiagramDirective(tree: Tree) = tree is + Tree.Define(Tree.DefineKind.Directive, [Tree.Ident("diagram", _), _] :: Nil) + +fun parsePrecedenceTree(tree: Tree) = if tree is + Tree.Ident("None", _) then None + Tree.App(Tree.Ident("Some", _), Tree.Literal(Token.LiteralKind.Integer, value)) then + Some(parseInt(value, 10)) + +fun extendKeyword(tree: Tree) = + if tree is Tree.Tuple(keyword :: leftPrec :: rightPrec :: Nil) and + keyword is Tree.Literal(Token.LiteralKind.String, name) then + let leftPrec' = parsePrecedenceTree(leftPrec) + let rightPrec' = parsePrecedenceTree(rightPrec) + Keywords.keyword(name, leftPrec', rightPrec') + else print of "expect a string literal but found " + keyword + else print of "expect a tuple but found " + tree + +// * Add an empty parse rule to the map associated with the given name. +fun newCategory(tree: Tree) = + if tree is Tree.Literal(Token.LiteralKind.String, name) and + Rules.syntaxKinds |> MutMap.get(name) is + Some(rule) then print of "Category already exists: " + rule.display + None then + Rules.syntaxKinds |> MutMap.insert(name, ParseRule.ParseRule(name, Nil)) + Rules.extendedKinds.add(name) + else + print of "expect a string literal but found " + tree + +pattern OpenCategory = "term" | "type" | "decl" +pattern ClosedCategory = "ident" | "typevar" + +fun extendCategory(choiceBodyTree: Tree) = + if parseChoiceTree(choiceBodyTree) is + Some([kindName, choice]) and Rules.syntaxKinds |> MutMap.get(kindName) is + Some(rule) and kindName is + // If the `kindName` is built-in and extensible, we check if the choice + // refers to another category in the beginning. If so, we inline the + // referenced category's choices into the current choice. + OpenCategory and choice is + Choice.Ref(refKindName, process, outerPrec, innerPrec, rest) and + Rules.syntaxKinds |> MutMap.get(refKindName) is Some(refRule) then + // TODO: The `process` function should be applied to the referenced rule. + // TODO: How to handle `outerPrec` and `innerPrec`? + rule.extendChoices of refRule.andThen(rest, process).choices + else + // The referenced category is not found in the map. Stop. + print of "Unknown referenced syntax category: " + refKindName + else + // Otherwise, we can directly extend the target parse rule. + rule.extendChoices(choice :: Nil) + ClosedCategory then print of "Cannot extend a closed category: " + kindName + else rule.extendChoices(choice :: Nil) + None then print of "Unknown syntax kind: " + kindName + None then + print of "Invalid syntax description: " + choiceBodyTree Tree.summary() + +fun parseChoiceTree(tree: Tree) = + fun go(trees: Stack[Tree]): Choice[Tree] = + let res = if trees is + Tree.App(Tree.Ident("keyword", _), Tree.Literal(LiteralKind.String, name)) :: rest and + Keywords.all |> MutMap.get(name) is Some(keyword) then + Choice.keyword(keyword)(go(rest)) + Tree.Literal(LiteralKind.String, name) :: rest then + Choice.reference(name)(process: Cons, name: "unnamed", choices: tuple of go(rest)) + Nil then Choice.end(Nil) + res + + if tree is + Tree.Tuple(categoryIdent :: choiceTree :: funcIdent :: Nil) and + categoryIdent is Tree.Literal(LiteralKind.String, categoryName) and + funcIdent is Tree.Ident and + let op = (trees) => trees + Iter.fromStack() + Iter.folded of funcIdent, (f, x) => Tree.App(f, x) + choiceTree is + Tree.Bracketed(Token.Square, Tree.Tuple(elements)) then + Some of tuple of + categoryName + go(elements) Choice.map(op) + Tree.Bracketed(Token.Square, other) then + Some of tuple of + categoryName + go(other :: Nil) Choice.map(op) + else + print of "Expect the choiceTree to be a bracketed term but found", choiceTree Tree.summary() + None + else + print of "Expect a the category to be an identifier but found " + categoryIdent Tree.summary() + None + else + print of "Expect the definition to be a tuple but found " + tree Tree.summary() + None diff --git a/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/Keywords.mls b/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/Keywords.mls new file mode 100644 index 0000000000..9a1a52f828 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/Keywords.mls @@ -0,0 +1,179 @@ +import "std/Option.mls" +import "std/Iter.mls" +import "std/Predef.mls" +import "std/MutMap.mls" + +open Predef +open Option { Some, None } + +module Keywords with ... + +// https://v8.dev/blog/pointer-compression +// The maximal and minimal values of V8's Smi on 64-bit platforms. +val INT_MIN = -2147483648 +val INT_MAX = 2147483647 + +class Keyword(val name: Str, val leftPrec, val rightPrec) with + fun leftPrecOrMin = if leftPrec is Some(prec) then prec else INT_MIN + fun rightPrecOrMin = if rightPrec is Some(prec) then prec else INT_MIN + fun leftPrecOrMax = if leftPrec is Some(prec) then prec else INT_MAX + fun rightPrecOrMax = if rightPrec is Some(prec) then prec else INT_MAX + + fun toString() = fold(+) of + "Keyword(`", name, "`, " + (if leftPrec is Some(prec) then prec.toString() else "N/A"), ", ", + (if rightPrec is Some(prec) then prec.toString() else "N/A"), ")" + +fun makePrecMap(startPrec: Int, ...ops) = + let + m = MutMap.empty + i = 0 + while i < ops.length do + ops.at(i).split(" ").forEach of (op, _, _) => + if op.length > 0 do + m |> MutMap.insert of op, i + startPrec + set i += 1 + m + +val all = MutMap.empty +fun keyword(name, leftPrec, rightPrec) = + let result = Keyword(name, leftPrec, rightPrec) + all |> MutMap.insert(name, result) + result +let prec = 0 +fun currPrec = Some(prec) +fun nextPrec = + set prec = prec + 1 + Some(prec) +let basePrec = currPrec // the lowest precedence +val _terminator = keyword(";;", basePrec, basePrec) +val _class = keyword("class", None, basePrec) +let semiPrec = nextPrec +let commaPrec = nextPrec +val _semicolon = keyword(";", semiPrec, basePrec) +val _comma = keyword(",", commaPrec, semiPrec) +let eqPrec = nextPrec +let ascPrec = nextPrec +val _equal = keyword("=", eqPrec, eqPrec) +val _and = keyword("and", None, currPrec) +val _bar = keyword of "|", None, None +val _thinArrow = keyword of "->", nextPrec, eqPrec +val _colon = keyword(":", ascPrec, eqPrec) +val _match = keyword("match", nextPrec, currPrec) +val _while = keyword of "while", nextPrec, currPrec +val _for = keyword of "for", nextPrec, currPrec +val _to = keyword of "to", None, None +val _downto = keyword of "downto", None, None +val _do = keyword of "do", None, None +val _done = keyword of "done", None, None +val _of = keyword of "of", None, None +val _with = keyword("with", None, currPrec) +val _case = keyword("case", None, currPrec) +let thenPrec = nextPrec +val _if = keyword("if", nextPrec, thenPrec) +val _leftArrow = keyword of "<-", thenPrec, thenPrec +val _then = keyword("then", thenPrec, thenPrec) +val _else = keyword("else", thenPrec, thenPrec) +val _let = keyword("let", eqPrec, semiPrec) +val _in = keyword("in", thenPrec, thenPrec) +val _true = keyword("true", None, None) +val _false = keyword("false", None, None) +// val _fatArrow = keyword of "=>", nextPrec, eqPrec +val _as = keyword("as", nextPrec, currPrec) +val _fun = keyword("fun", currPrec, _thinArrow.leftPrec) +val _function = keyword("function", currPrec, eqPrec) +val _type = keyword("type", currPrec, None) +val _exception = keyword("exception", currPrec, None) +val _rec = keyword("rec", currPrec, eqPrec) +val _hash = keyword("#", None, None) +val maxKeywordPrec = prec + +let precMap = makePrecMap of + maxKeywordPrec + "," + "@" + ":" + "|" + "&" + "=" + "/ \\" + "^" + "!" + "< >" + "+ -" + "* %" + "~" + "" // prefix operators + "" // applications + "." + +// The largest precedence value of all operators. +val periodPrec = precMap |> MutMap.get(".") |> Option.unsafe.get + +val _period = keyword of ".", Some(periodPrec), Some(periodPrec) + +val maxOperatorPrec = periodPrec +val appPrec = maxOperatorPrec - 1 +val prefixPrec = appPrec - 1 + +// Get the precedence of a single character operator. +fun charPrec(op) = precMap |> MutMap.get(op) |> Option.unsafe.get + +fun charPrecOpt(op) = precMap |> MutMap.get(op) + +val _asterisk = keyword of "*", charPrecOpt("*"), charPrecOpt("*") +val _equalequal = keyword of "==", charPrecOpt("="), charPrecOpt("=") + +let bracketPrec = Some(maxOperatorPrec + 1) + +val _leftRound = keyword of "(", bracketPrec, basePrec +val _rightRound = keyword of ")", basePrec, None +val _leftSquare = keyword of "[", bracketPrec, basePrec +val _rightSquare = keyword of "]", basePrec, None +val _leftCurly = keyword of "{", bracketPrec, basePrec +val _rightCurly = keyword of "}", basePrec, None +val _begin = keyword of "begin", bracketPrec, basePrec +val _end = keyword of "end", basePrec, None + +let builtinKeywords = new Set(all |> MutMap.keysIterator) +fun extended = all + Iter.filtering of case [k, _] then builtinKeywords.has(k) is false + Iter.toArray() + MutMap.toMap() + +pattern Letter = "a" ..= "z" | "A" ..= "Z" + +fun hasLetter(s) = s Iter.some((ch, _, _) => ch is Letter) + +pattern FloatOperator = "+." | "-." | "*." | "/." + +pattern RightAssociative = "@" | "/" | "," | ":" + +fun opPrec(opStr) = if + opStr is FloatOperator then tuple of + Keywords.charPrec of opStr.at(0) + Keywords.charPrec of opStr.at(0) + opStr hasLetter() then tuple of + Keywords.maxKeywordPrec + Keywords.maxKeywordPrec + let lastChar = opStr.at(-1) + let rightPrec = Keywords.charPrec of lastChar + else tuple of + Keywords.charPrec of opStr.at(0) + (Keywords.charPrec of lastChar) + + if lastChar is RightAssociative then -1 else 0 + +fun opPrecOpt(opStr) = if + opStr is "" then None + opStr is FloatOperator then Some of tuple of + Keywords.charPrec of opStr.at(0) + Keywords.charPrec of opStr.at(0) + opStr hasLetter() then Some of tuple of + Keywords.maxKeywordPrec + Keywords.maxKeywordPrec + let lastChar = opStr.at(-1) + Keywords.charPrecOpt(lastChar) is Some(rightPrec) and + Keywords.charPrecOpt(opStr.at(0)) is Some(leftPrec) then Some of tuple of + leftPrec + rightPrec + (if lastChar is RightAssociative then -1 else 0) + else None diff --git a/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/Lexer.mls b/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/Lexer.mls new file mode 100644 index 0000000000..5ab8e10fbc --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/Lexer.mls @@ -0,0 +1,243 @@ +#config(liftDefns: Some(LiftDefns)) + +import "std/Predef.mls" + +open Predef + +import "std/Char.mls" +import "std/Stack.mls" +import "std/StrOps.mls" +import "std/Option.mls" +import "std/Iter.mls" + +import "./Token.mls" + +open Stack +open StrOps +open Option { Some, None} +open Token { LiteralKind, LineLookupTable } + +type TokenType = Token.Token +type Opt[T] = Some[T] | None + +module Lexer with ... + +class Location(start: Int, end: Int) + +class Message(description: Str, location: Location) + +class Report(messages: Stack[Message]) + +pattern IdentifierStart = Char.Letter | "_" + +pattern IdentifierBody = Char.Letter | Char.Digit | "_" | "'" + +pattern Operator = + "," | ";" + | "!" | "#" | "%" | "&" | "*" | "+" | "-" | "/" | ":" | "<" + | "=" | ">" | "?" | "@" | "\\" | "^" | "|" | "~" | "." + +pattern Bracket = "(" | ")" | "[" | "]" | "{" | "}" + +pattern IdentifierQuote = "'" | "`" + +fun makeLineLookupTable(text: Str): LineLookupTable = + let + i = 0 + n = text.length + ns = mut [] + while i < n do + let i' = text.indexOf("\n", i) + if i' == -1 then + set i = n + ns.push(n) + else + set i = i' + 1 + ns.push(i') + LineLookupTable(ns) + +fun lex(str: Str, options) = + using LineLookupTable = makeLineLookupTable(str) + + fun char(idx: Int) = if idx < str.length + then (Some of str.charAt of idx) else None + + // Consume a sequence of characters that satisfy a predicate. + // The function returns the accumulated string and the following index. + fun take(pred: Str -> Bool, idx: Int, acc: Str): [Int, Str] = + while (char of idx) is Some(ch) and pred(ch) do + set + idx += 1 + acc += ch + [idx, acc] + + fun whitespace(idx: Int): Int = + while char(idx) is Some(Char.Whitespace) do + set idx = idx + 1 + idx + + fun digits(idx: Int, acc: Str) = + while char(idx) is Some(Char.Digit as ch) do + set idx = idx + 1 + set acc = acc + ch + [idx, acc] + + fun hex(idx: Int, acc: Str) = + while char(idx) is Some(Char.Digit as ch) do + set idx = idx + 1 + set acc = acc + ch + [idx, acc] + + fun identifier(idx: Int, acc: Str) = + while char(idx) is Some(IdentifierBody as ch) do + set idx = idx + 1 + set acc = acc + ch + tuple of + idx + if acc is + "true" then Token.boolean("true", idx) + "false" then Token.boolean("false", idx) + else Token.identifier(acc, idx) + + fun operator(idx: Int, acc: Str) = + while char(idx) is Some(Operator as ch) do + set idx = idx + 1 + set acc = acc + ch + [idx, Token.symbol(acc, idx)] + + fun comment(idx: Int) = + let start = idx + let content = "" + if char(idx) is + Some("/") then + set idx = idx + 1 + while char(idx) is Some(ch) + and ch !== "\n" do + set idx = idx + 1 + set content = content + ch + [idx, Token.comment(content, start, idx)] + Some("*") then + let terminated = false + set idx = idx + 1 + while terminated is false and char(idx) is + Some("*") and char(idx + 1) is Some("/") then + set idx = idx + 2 + set terminated = true + Some(ch) then + set idx = idx + 1 + set content = content + ch + if terminated then + [idx, Token.comment(content, start, idx)] + else + [idx, Token.error(start, idx)] + else operator(idx, "/") + + fun scanHexDigits(idx: Int, lim: Int, acc: Int, cnt: Int): [Int, Int, Int] = + if char(idx) is Some(Char.HexDigit as ch) and + cnt < lim then scanHexDigits(idx + 1, lim, acc * 16 + parseInt(ch, 16), cnt + 1) + else scanHexDigits(idx + 1, lim, acc, cnt + 1) + else [idx, acc, cnt] + + fun escape(idx: Int): [Int, Opt[Str]] = if char(idx) is + Some("n") then [idx + 1, Some("\n")] + Some("r") then [idx + 1, Some("\r")] + Some("t") then [idx + 1, Some("\t")] + Some("0") then [idx + 1, Some("\u{0}")] + Some("b") then [idx + 1, Some("\b")] + Some("f") then [idx + 1, Some("\f")] + Some("\"") then [idx + 1, Some("\"")] + Some("\\") then [idx + 1, Some("\\")] + Some("x") then + if scanHexDigits(idx + 1, 2, 0, 0) is [idx, cp, cnt] then ... + // TODO: ensure that `cnt == 2` + tuple of idx, if cnt is 0 then None else Some of String.fromCodePoint(cp) + Some("u") and + // Unicode code point escape: "\u{XXXXXX}" + char(idx + 1) is Some("{") then + if scanHexDigits(idx + 2, 6, 0, 0) is [idx, cp, cnt] then ... + // TODO: ensure that `1 <= cnt <= 6` + let idx = if char(idx) is Some("}") then idx + 1 else + // TODO: report missing "}" + idx + tuple of idx, if cnt is 0 then None else Some of String.fromCodePoint(cp) + // Trandition Unicode range: "\uXXXX" + else + if scanHexDigits(idx + 1, 4, 0, 0) is [idx, cp, cnt] then ... + // TODO: ensure that `cnt == 4` + tuple of idx, if cnt is 0 then None else Some of String.fromCodePoint(cp) + Some(ch) then [idx + 1, Some(ch)] + None then [idx, None] + + fun string(idx: Int): [Int, Token.Literal] = + let + startIndex = idx + content = "" + terminated = false + while terminated is false do + if char(idx) is + Some("\"") do + set + terminated = true + idx += 1 + Some("\\") do + if escape(idx + 1) is [idx', chOpt] then ... + set idx = idx' + if chOpt is Some(ch) do set content += ch + Some(ch) do + set + idx += 1 + content += ch + None do + // TODO report missing quote + set terminated = true + [idx, Token.string(content, startIndex, idx)] + + fun number(idx: Int, head: Str) = if + head is "0" and char(idx) is + None then [idx, Token.integer("0", idx)] + Some("b") and take(x => x is Char.BinDigit, idx + 1, "") is [idx', bs] then + [idx', Token.integer("0b" ~ bs, idx)] + Some("o") and take(x => x is Char.OctDigit, idx + 1, "") is [idx', os] then + [idx', Token.integer("0o" ~ os, idx)] + Some("x") and take(x => x is Char.HexDigit, idx + 1, "") is [idx', xs] then + [idx', Token.integer("0x" ~ xs, idx)] + Some(".") and digits(idx + 1, ".") is [idx', ds] then + [idx', Token.decimal("0." ~ ds, idx)] + Some(_) and digits(idx, head) is [idx', integer] then + [idx', Token.integer(integer, idx)] + digits(idx, head) is [idx', integer] and + char(idx') is Some(".") and digits(idx' + 1, "") is [idx'', fraction] then + [idx'', Token.decimal(integer ~ "." ~ fraction, idx)] + else [idx', Token.integer(integer, idx)] + + fun scan(idx: Int, acc: Stack[TokenType]): Stack[TokenType] = + fun go(idx: Int, tok: TokenType) = if + options.noWhitespace and tok is + Token.Comment then scan(idx, acc) + Token.Space then scan(idx, acc) + else scan(idx, tok :: acc) + + fun go_tup(tup: [Int, TokenType]) = go(tup.0, tup.1) + + if char(idx) is + None then reverse of acc + Some(ch) and ch is... + Char.Whitespace and whitespace(idx) is idx' then + go(idx', Token.space(idx, idx')) + "\"" then go_tup(string(idx + 1)) + Bracket as b then go(idx + 1, Token.symbol(b, idx)) + "/" then go_tup(comment(idx + 1)) + Operator as ch then go_tup(operator(idx + 1, ch)) + Char.Digit as ch then go_tup(number(idx + 1, ch)) + IdentifierStart as ch then go_tup(identifier(idx + 1, ch)) + IdentifierQuote as quote + and char(idx + 1) is Some(IdentifierStart as ch) + and identifier(idx + 2, quote + ch) is [idx', token] and + token is Token.Identifier(name, _) then + go(idx', Token.identifier(name, idx)) + else go(idx + 1, Token.error(idx, idx + 1)) + else + print("Unrecognized character: '" ~ ch ~ "'") + go(idx + 1, Token.error(idx, idx + 1)) + + scan(0, Nil) diff --git a/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/ParseRule.mls b/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/ParseRule.mls new file mode 100644 index 0000000000..98f9056afd --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/ParseRule.mls @@ -0,0 +1,298 @@ +import "std/MutMap.mls" +import "std/Iter.mls" +import "std/Option.mls" +import "std/Stack.mls" +import "std/Predef.mls" +import "./Keywords.mls" +import "./Token.mls" +import "./Tree.mls" + +open Option { Some, None } +open Predef { check, tuple } +open Stack + +module ParseRule with ... + +abstract class Choice[A] +module Choice with + + data class Keyword[A](keyword: Keywords.Keyword, rest: ParseRule[A]) extends Choice[A] + + data class Ref[A, B]( + kind: Str, + process: (Tree, B) -> A, + outerPrec: Option[Int], + innerPrec: Option[Int], + rest: ParseRule[B] + ) extends Choice[A] + + data class End[A](value: A) extends Choice[A] + + // * An alternative route that branches off from the main railroad and + // * eventually connects back to the main railroad. For example, `foo`, `bar`, + // * and `baz` are choices represented by `init`. If `optional` is set to + // * `true`, an additional empty choice is added to `rule`, making it possible + // * to skip the entire `rule`. + // * _______ foo ______ + // * / \ + // * /-------- bar -------\ + // * / \ + // * ----- start ----+---------- baz ---------+--- end --------- + data class Siding[A, B, C]( + init: ParseRule[B], + optional: Bool, + rest: ParseRule[C], + process: (Option[B], C) -> A + ) extends Choice[A] + + let ensureChoices(xs, name) = xs + Iter.zippingWithIndex() + Iter.each of case [item, index] then + check(item is Choice, name + ": element [" + index + "] is not Choice") + + // Shorthands for constructing rule choices. + fun keyword(keyword)(...choices) = + ensureChoices(choices, "Choice.keyword") + Keyword(keyword, rule("`" + keyword.name + "` keyword", ...choices)) + + let shouldHaveFunction(options, key: Str, defaultValue, callerName: Str) = if + let func = options.(key) + typeof(func) === "function" then func + func is undefined then defaultValue + else throw TypeError(callerName + ": `" + key + "` is not a string") + + let shouldHaveStr(options, key: Str, defaultValue: Str, callerName: Str) = if + let value = options.(key) + value is Str then value + value is undefined then defaultValue + else throw TypeError(callerName + ": `" + key + "` is not a string") + + let shouldHaveInt(options, key: Str, callerName: Str) = if + let value = options.(key) + value is Int then Some(value) + value is undefined then None + else throw TypeError(callerName + ": `" + key + "` is not an Int") + + let shouldHaveBool(options, key: Str, defaultValue: Bool, callerName: Str) = if + let value = options.(key) + value is Bool then value + value is undefined then defaultValue + else throw TypeError(callerName + ": `" + key + "` is not a Boolean") + + let shouldHaveRuleLike(options, key: Str, ruleName: Str, callerName: Str) = if + let choices = options.(key) + choices is ParseRule then choices + choices is Choice then rule(ruleName, choices) + choices is [..._] then + ensureChoices(choices, "Choice.reference") + rule(ruleName, ...choices) + choices is undefined then rule(ruleName) + else throw TypeError(callerName + ": `" + key + "` is neither a rule nor a choice") + + fun reference(kind: Str)(fields: Object) = + let ruleName = fields shouldHaveStr("name", "unnamed", "Choice.reference") + Ref of + kind + fields shouldHaveFunction("process", tuple, "Choice.reference") + fields shouldHaveInt("outerPrec", "Choice.reference") + fields shouldHaveInt("innerPrec", "Choice.reference") + fields shouldHaveRuleLike("choices", ruleName, "Choice.reference") + + val term = reference("term") + val typeExpr = reference("type") + val ident = reference("ident") + val typeVar = reference("typevar") + + fun optional(init, rest) = + check(init is ParseRule, "Choice.optional: init is not ParseRule") + check(rest is ParseRule, "Choice.optional: rest is not ParseRule") + Siding(init, true, rest, tuple) + + fun siding(fields) = + let optional = fields shouldHaveBool("optional", false, "Choice.siding") + let initName = fields shouldHaveStr("initName", "unnamed", "Choice.siding") + let restName = fields shouldHaveStr("restName", "unnamed", "Choice.siding") + let init = fields shouldHaveRuleLike("init", initName, "Choice.siding") + let rest = fields shouldHaveRuleLike("rest", restName, "Choice.siding") + let defaultProcess = if optional then tuple + else (initRes, restRes) => if initRes is Some(initRes) then [initRes, restRes] + let process = fields shouldHaveFunction("process", defaultProcess, "Choice.siding") + Siding of init, optional, rest, process + + fun end(value) = End(value) + + fun map[A, B](choice: Choice[A], op: A -> B): Choice[B] = if choice is + Keyword(keyword, rest) then Keyword(keyword, rest.map(op)) + Ref(kind, process, outerPrec, innerPrec, rest) then + Ref(kind, (x, y) => op(process(x, y)), outerPrec, innerPrec, rest) + Siding(init, optional, rest, process) then + Siding(init, optional, rest, (x, y) => op(process(x, y))) + End(value) then End(op(value)) + +data class Lazy[out A](init: () -> A) with + mut val cached: Option[A] = None + fun reset() = set cached = None + fun get() = if cached is + Some(v) then v + else + let v = init() + set cached = Some(v) + v + +fun lazy(init) = new Lazy of init + +// ____ ____ _ +// | _ \ __ _ _ __ ___ ___| _ \ _ _| | ___ +// | |_) / _` | '__/ __|/ _ \ |_) | | | | |/ _ \ +// | __/ (_| | | \__ \ __/ _ <| |_| | | __/ +// |_| \__,_|_| |___/\___|_| \_\\__,_|_|\___| +// +// ============================================= + +data class ParseRule[A](name: Str, mut val choices: Stack[Choice[A]]) with + open Choice { Keyword, Ref, End, Siding } + + fun map[B](op: A -> B) = + new mut ParseRule of name, choices + Iter.fromStack() + Iter.mapping of choice => choice Choice.map(op) + Iter.toStack() + + fun andThen[B, C](rest: ParseRule[B], process: (A, B) -> C) = + fun go(rule: ParseRule[B]) = new ParseRule of rule.name, rule.choices + Iter.fromStack() + Iter.mapping of case + Keyword(keyword, rest') then [Keyword(keyword, go(rest'))] + Ref(kind, process, outerPrec, innerPrec, rest') then + let process' = (lhs, rhsInnerResult) => if + rhsInnerResult is [rhs, innerResult] then + [process(lhs, rhs), innerResult] + do check(false, "illgeal result from inner") + [Ref(kind, process', outerPrec, innerPrec, go(rest'))] + End(value) then rest.choices + Iter.fromStack() + Iter.mapping of choice => choice Choice.map(result => [value, result]) + Siding(rule, optional, rest', process) then + let process' = (initRes, restRes) => if + restRes is [restRes', innerRes] then [process(initRes, restRes'), innerRes] + do check(false, "illegal result from inner") + [Siding(rule, optional, go(rest'), process')] + Iter.flattening() + Iter.toStack() + go(this).map of res => process(res.0, res.1) + + let _endChoice = lazy of () => choices + Iter.fromStack() + Iter.firstDefined of case + End(value) then Some(value) + Siding(init, optional, rest, process) and + optional and rest.endChoice is Some(restRes) then + // It's actually ambiguous here. + process(None, restRes) + init.endChoice is Some(initRes) and + rest.endChoice is Some(restRes) then + process(Some(initRes), restRes) + else None + + // Collect the first end choice in this rule. + fun endChoice = _endChoice.get() + + let _keywordChoices = lazy of () => choices + Iter.fromStack() + Iter.mapping of case + Keyword(keyword, rest) then [[keyword.name, rest]] + Siding(init, optional, rest, process) then init.keywordChoices + Iter.mapping of case [keyword, rule] then + [keyword, rule.map(Some).andThen(rest, process)] + Iter.appended of + if optional then rest.keywordChoices + Iter.mapping of case [keyword, rule] then + [keyword, rule.map(res => process(None, res))] + else [] + Iter.toArray() + else [] + Iter.flattening() + Iter.toArray() + MutMap.toMap() + + // Collect all keyword choices in this rule. + fun keywordChoices = _keywordChoices.get() + + let _refChoice = lazy of () => choices + Iter.fromStack() + Iter.firstDefined of case + Ref as ref then Some(ref) + Siding(init, optional, rest, process) and + init.refChoice is Some(Ref(k, process', op, ip, rest')) then + let process''(exprRes, pairRes) = + if pairRes is [restRes', restRes] then + process(process'(exprRes, restRes'), restRes) + let rest'' = rest'.andThen(rest, tuple) + Some of Ref(k, process'', op, ip, rest'') + optional and rest.refChoice is Some(Ref(k, process', op, ip, rest')) then + let process''(exprRes, restRes) = + process(None, process'(exprRes, restRes)) + Some of Ref(k, process'', op, ip, rest') + else None + + fun refChoice = _refChoice.get() + + fun extendChoices(newChoices: Stack[Choice[A]]) = + set choices = choices ::: newChoices + _endChoice.reset() + _keywordChoices.reset() + _refChoice.reset() + this + + // Display parse rules as a tree in a BNF-like format. + fun display = + /// Display a single `Choice`. + fun displayChoice[A](choice: Choice[A]) = if choice is + Choice.Keyword(keyword, rest) then + "\"" + keyword.name + "\"" + tail(rest).1 + Choice.Ref(kind, _, _, _, rest) then + "<" + kind + ">" + tail(rest).1 + Choice.Siding(init, opt, rest, _) then + let init' = go(init, false).1 + (if opt then "[" + init' + "]" else "(" + init' + ")") + tail(rest).1 + Choice.End then "" + other then "" + + // Display the remaining list of choices. + // ++ fun tail(rest) = if rest is ParseRule(_, choices) and + fun tail(rest) = if rest is ParseRule and // --- + let choices = rest.choices // --- + choices is Choice.End :: Nil then ["", ""] + go(rest, false) is [name, line] and + choices is _ :: _ :: _ and + choices Iter.fromStack() Iter.some(c => c is Choice.End) then + [name, " [" + line + "]"] + else [name, " (" + line + ")"] + else [name, " " + line] + + fun go(rule, top) = + let lines = rule.choices + Iter.fromStack() + Iter.filtering of case + Choice.End then false + else true + Iter.mapping of displayChoice + Iter.toArray() + tuple of + rule.name + if + lines is [] then "Ξ΅" + lines is [line] then line + top then "\n | " + lines.join("\n | ") + else lines.join(" | ") + + if go(this, true) is [name, line] then "<" + name + "> ::= " + line + +// Shorthands for constructing parse rules. +// Automatically adds an end choice to the rule if no choices are provided. +fun rule(name, ...choices) = new ParseRule of + name + if choices.length == 0 then + Choice.end(()) :: Nil + else + choices Iter.toStack() diff --git a/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/ParseRuleVisualizer.mls b/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/ParseRuleVisualizer.mls new file mode 100644 index 0000000000..c0cbe88c0f --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/ParseRuleVisualizer.mls @@ -0,0 +1,94 @@ +import "std/Predef.mls" +import "std/Stack.mls" +import "std/Iter.mls" +import "std/Option.mls" +import "std/TreeTracer.mls" +import "std/XML.mls" +import "std/MutMap.mls" + +import "./ParseRule.mls" +import "./Rules.mls" +import "./Parser.mls" + +open Predef +open Stack +open Option +open ParseRule { Choice } +open Rules { syntaxKinds } +open TreeTracer +open XML { html, tag, style } + +module ParseRuleVisualizer with ... + +val tracer = new TreeTracer.TreeTracer() + +let defaultKinds = tuple of "type", "term", "typevar", "ident" + +let renderedKinds = new Set of defaultKinds + +fun reset() = + set renderedKinds = new Set of defaultKinds + +// * `rr` is the railroad library. +fun render[T](rr, title: Str, rule: ParseRule.ParseRule[T]) = + let helperRules = mut [] + let referencedKinds = new Set() + let renderCache = new Map() + fun sequence(lhs, rhsOpt) = + if rhsOpt is + Some(rhs) then rr.Sequence(lhs, rhs) + None then lhs + fun diagram(choicesOpt) = + rr.Diagram of if choicesOpt is Some(choices) then choices else [] + fun renderChoice(parentRule, choice) = if choice is + Choice.End then + tracer.print of "found Choice.End" + None + Choice.Keyword(keyword, rest) then + tracer.print of "found Choice.Keyword" + Some of rr.Terminal(keyword.name) sequence(renderRule(rest)) + Choice.Siding(rule, optional, rest, _) then + tracer.print of "found Choice.Siding" + Some of if renderRule(rule) is + let latterPart = renderRule(rest) + Some(optionalPart) and + optional then rr.Optional(optionalPart) sequence(latterPart) + else optionalPart sequence(latterPart) + None then latterPart + Choice.Ref(kind, _, outerPrec, innerPrec, rest) then + tracer.print of "found Choice.Ref to " + kind + if renderedKinds.has(kind) is false do + referencedKinds.add of kind + Some of rr.NonTerminal(kind, href: "#" + kind) + sequence(renderRule(rest)) + fun renderRule(rule: ParseRule.ParseRule[T]) = tracer.trace of + "renderRule <<< " + rule.name + result => "renderRule >>> " + () => ... + let + rest = rule.choices + optional = false + nodes = mut [] + while rest is head :: tail do + if renderChoice(rule, head) is + Some(node) do nodes.push of node + None do set optional = true + set rest = tail + tracer.print of "nodes: ", nodes.length.toString() + if + nodes.length is 0 then None + let choice = rr.Choice(0, ...nodes) + optional is true then Some(rr.Optional(choice)) + else Some(choice) + // Iteratively render the rule and its dependencies. + let diagrams = mut [[title, diagram of renderRule(rule)]] + while referencedKinds.size > 0 do + let currentKinds = referencedKinds + set renderedKinds = renderedKinds.union of currentKinds + referencedKinds = new Set() + diagrams.push of ...currentKinds + Iter.mapping of kind => + let theRule = syntaxKinds |> MutMap.get(kind) |> Option.unsafe.get + [kind, diagram of renderRule(theRule)] + Iter.toArray() + diagrams diff --git a/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/Parser.mls b/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/Parser.mls new file mode 100644 index 0000000000..6d7a0b03e9 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/Parser.mls @@ -0,0 +1,286 @@ +import "std/Predef.mls" +import "std/Option.mls" +import "std/Stack.mls" +import "std/TreeTracer.mls" +import "std/Iter.mls" +import "std/MutMap.mls" + +import "./Lexer.mls" +import "./Extension.mls" +import "./Token.mls" +import "./TokenHelpers.mls" +import "./Keywords.mls" +import "./Tree.mls" +import "./Rules.mls" +import "./ParseRule.mls" + +open Predef +open Option +open Stack +open ParseRule { Choice } +open Choice { Ref } +open Keywords { opPrecOpt } +open Rules { termRule, typeRule, declRule, syntaxKinds } + +type Opt[A] = Some[A] | None +type TreeT = Tree +type StackT[A] = Stack.Cons[A] | Stack.Nil + +module Parser with ... + +val tracer = new TreeTracer.TreeTracer + +let termOptions = + kind: "term", rule: termRule + allowOperators: true, allowLiterals: true +let typeOptions = + kind: "type", rule: typeRule + allowOperators: false, allowLiterals: true, + +fun parse(tokens) = + let counter = 0 + + fun consume = + if tokens is head :: tail then + tracer.print of "consume: `" + Token.summary(head) + "` at #" + counter + set tokens = tail + set counter = counter + 1 + else + tracer.print of "consume: the end of input" + + fun parseKind(kind: Str, prec: Int): TreeT = if + kind is "type" then expr(prec, typeOptions) + kind is "term" then expr(prec, termOptions) + // built-in kinds: plain identifiers + kind is "ident" then if tokens is + Token.Identifier(name, false) :: _ and Keywords.all |> MutMap.get(name) is None then + consume + Tree.Ident(name, false) + token :: _ then Tree.error("expect an identifier but found " + token) + Nil then Tree.error("expect an identifier but found the end of input") + // built-in kinds: type variables + kind is "typevar" then if tokens is + Token.Identifier(name, false) :: _ and name.at(0) is "'" then + consume + Tree.Ident(name, false) + token :: _ then Tree.error("expect a type variable but found " + token) + Nil then Tree.error("expect a type variable but found the end of input") + // * other rule-based kinds + syntaxKinds |> MutMap.get(kind) is Some(rule) then + let tree = parseRule(prec, rule) + if rule.refChoice is Some(Ref(kind', process, None, None, rest)) and kind == kind' do + let shouldParse = true + while shouldParse do + let tree' = parseRule(prec, rest) + if tree' Tree.nonEmpty() then + tracer.print of ">>> " + kind + "Cont " + prec + " " + tree Tree.summary() + " <<<", source.line + set tree = process(tree, tree') + else + set shouldParse = false + tree + else throw Error("Unknown syntax kind: \"" + kind + "\"") + + fun parseRule(prec: Int, rule) = tracer.trace of + "parsing rule \"" + rule.name + "\" with precedence " + prec + result => "parsed rule \"" + rule.name + "\": " + result Tree.summary() + () => if tokens is... + Token.Identifier(name, _) :: _ and + do tracer.print of "found an identifier \"" + name + "\"", source.line + Keywords.all |> MutMap.get(name) is Some(keyword) and + do tracer.print of keyword.toString(), source.line + do tracer.print of "keyword choices: ", rule.keywordChoices + Iter.mapping of case [k, v] then "`" + k + "`" + Iter.joined(", ") + rule.keywordChoices |> MutMap.get(name) is + Some(rest) then + tracer.print of "found a rule starting with `" + name + "`", source.line + tracer.print of "the rest of the rule: " + rest.display, source.line + consume + parseRule(0, rest) + do tracer.print of "\"" + name + "\" is not a keyword", source.line + other :: _ and + do tracer.print of "the current rule is " + rule.display + rule.refChoice is Some(Ref(kind, process, outerPrec, innerPrec, rest)) and + do tracer.print of "try to parse kind \"" + kind + "\" at " + TokenHelpers.preview(tokens), source.line + let outerPrec' = outerPrec Option.getOrElse(Keywords.maxKeywordPrec) + let innerPrec' = innerPrec Option.getOrElse(prec) + outerPrec' > prec and + let acc = parseKind(kind, prec) + acc Tree.nonEmptyError() and parseRule(prec, rest) is + Tree.Error(_, message) as tree then + do tracer.print of "cannot parse due to error: " + message, source.line + tree + tree then + tracer.print of "acc: " + acc Tree.summary(), source.line + tracer.print of "parsed from rest rule: " + tree Tree.summary(), source.line + process(acc, tree) + do tracer.print of "cannot parse more", source.line + rule.endChoice is Some(value) then + tracer.print of "found end choice", source.line + value + do tracer.print of "no end choice", source.line + else acc + do tracer.print of "did not parse kind \"" + kind + "\" because of the precedence", source.line + do tracer.print of "no reference choice", source.line + rule.endChoice is Some(value) then + tracer.print of "found end choice", source.line + value + do tracer.print of "no end choice", source.line + else + consume + Tree.error("unexpected token " + render(other)) // TODO: Pretty-print the token. + Nil and rule.endChoice is + Some(value) then value + None then + tracer.print of "no end choice but found the end of input", source.line + Tree.error("unexpected end of input") + + fun expr(prec: Int, options): TreeT = tracer.trace of + options.kind + " <<< " + prec + " " + TokenHelpers.preview(tokens) + result => options.kind + " >>> " + result Tree.summary() + () => if tokens is ... + Token.Identifier(name, symbolic) :: _ and Keywords.all |> MutMap.get(name) is + // * the keyword case + Some(keyword) and options.rule.keywordChoices |> MutMap.get(name) is + Some(rule) and + keyword.leftPrecOrMin > prec then + consume + parseRule(keyword.rightPrecOrMax, rule) exprCont(prec, options) + else + tracer.print of "the left precedence of \"" + name + "\" is less", source.line + Tree.empty + None then + tracer.print("no rule starting with " + name, source.line) + Tree.empty + // * the non-keyword case + None then + if not options.allowOperators and symbolic then + Tree.error("symbolic identifiers are disallowed in kind \"" + options.kind + "\"") + else + consume + Tree.Ident(name, symbolic) exprCont(prec, options) + // * the literal case + Token.Literal(kind, literal) :: _ and options.allowLiterals then + consume + Tree.Literal(kind, literal) exprCont(prec, options) + // * other cases + token :: _ then Tree.error("unrecognized token: " + token) + Nil then Tree.error("unexpected end of input") + + fun exprCont(acc: TreeT, prec: Int, options) = if tokens is + let infix = options.rule.refChoice Option.flatMap of case + Ref(kind, process, None, None, rest) and kind == options.kind then + { process: process, rule: rest } + else throw Error("Kind " + options.kind + " does not have infix rules") + do tracer.print of ">>> " + options.kind + "Cont " + prec + " " + acc Tree.summary() + " <<<", source.line + // * the case of infix keyword rules + do tracer.print of "check keyword " + TokenHelpers.preview(tokens), source.line + Token.Identifier(name, _) :: _ and Keywords.all |> MutMap.get(name) is Some(keyword) and + do tracer.print of "found a keyword: " + name, source.line + infix.rule.keywordChoices |> MutMap.get(name) is Some(rule) and + do tracer.print of "keyword `" + name + "` is found in infix rules", source.line + keyword.leftPrecOrMin > prec and rule.refChoice is + Some(Ref(kind, process, outerPrec, innerPrec, rest)) and + do tracer.print of "try to parse kind \"" + kind + "\" at " + TokenHelpers.preview(tokens), source.line + let outerPrec' = outerPrec Option.getOrElse(Keywords.maxOperatorPrec) + let innerPrec' = innerPrec Option.getOrElse(outerPrec') + outerPrec' > prec then + consume + let rhs = parseKind(kind, keyword.rightPrecOrMin) + let restRes = parseRule(innerPrec', rest) + infix.process(acc, process(rhs, restRes)) exprCont(prec, options) + None then acc + do tracer.print of "keyword `" + name + "` does not have infix rules", source.line + // * the case of infix operators (non-keywords) + Token.Identifier(name, true) :: _ and Keywords.all |> MutMap.get(name) is None and options.allowOperators and + do tracer.print of "found an operator \"" + name + "\"", source.line + opPrecOpt(name) is Some([leftPrec, rightPrec]) and + do tracer.print of "leftPrec = " + leftPrec + "; rightPrec = " + rightPrec, source.line + leftPrec > prec then + consume + let op = Tree.Ident(name, true) + let rhs = expr(rightPrec, termOptions) + Tree.App(op, acc :: rhs :: Nil) exprCont(prec, options) + else + acc + // * the case of application + do tracer.print of "not a keyword", source.line + token :: _ and infix.rule.refChoice is + Some(Ref(kind, process, outerPrec, innerPrec, rest)) and + do tracer.print of "found reference to " + kind + " with outerPrec = " + outerPrec, source.line + let outerPrec' = outerPrec Option.getOrElse(Keywords.maxOperatorPrec) + let innerPrec' = innerPrec Option.getOrElse(outerPrec') + outerPrec' > prec and + parseKind(kind, innerPrec Option.getOrElse(outerPrec')) is + Tree.Empty then + do tracer.print of "nothing was parsed", source.line + acc + Tree.Error then + do tracer.print of "cannot parse more", source.line + acc + rhs then + do tracer.print of "parsed " + rhs Tree.summary(), source.line + let restRes = parseRule(innerPrec', rest) + infix.process(acc, process(rhs, restRes)) exprCont(prec, options) + do tracer.print of "the outer precedence is less than " + prec, source.line + else acc + None then + tracer.print of "cannot consume " + token, source.line + acc + Nil then acc + + fun handleDirective(tree: TreeT, acc: TreeT) = if tree is + // Call the corresponding handler for the directive. + Tree.Define(Tree.DefineKind.Directive, [name, body] :: Nil) as tree and name is + Tree.Ident("newKeyword", _) then + Extension.extendKeyword(body) + modCont(acc) + Tree.Ident("newCategory", _) then + Extension.newCategory(body) + modCont(acc) + Tree.Ident("extendCategory", _) then + Extension.extendCategory(body) + modCont(acc) + else + modCont(tree :: acc) + tree then modCont(tree :: acc) + + fun mod(acc: StackT[TreeT]) = if tokens is + do tracer.print of ">>>>>> mod <<<<<<", source.line + Token.Identifier(";;", _) :: _ then + consume + mod + Token.Identifier(name, _) :: _ and + Keywords.all |> MutMap.get(name) is Some(keyword) and + // First, try if the keyword is the prefix of the term. + termRule.keywordChoices |> MutMap.get(name) is Some(rule) then + let tree = expr(0, termOptions) + if tree is Tree.LetIn(bindings, Tree.Empty) then + // If the body is empty, then this is a let-definition. + modCont of Tree.Define(Tree.DefineKind.Let(false), bindings) :: acc + else + modCont of tree :: acc + // Then try to parse according to the rules of the definition. + declRule.keywordChoices |> MutMap.get(name) is Some(rule) then + consume + parseRule(0, rule) handleDirective(acc) + _ :: _ then modCont of expr(0, termOptions) :: acc + Nil then acc reverse() + + fun modCont(acc: StackT[TreeT]) = if tokens is + do tracer.print of ">>>>>> modCont <<<<<< " + TokenHelpers.preview(tokens), source.line + Token.Identifier(";;", _) :: _ then consume; acc mod() + _ :: _ then parseRule(0, declRule) handleDirective(acc) + Nil then acc reverse() + + let tree = tracer.trace of + "module <<< " + result => "module >>> " + result Tree.summary() + () => mod(Nil) + + if tokens is + token :: _ then + let message = "expect EOF instead of " + token + tracer.print of message, source.line + tree Tree.Error(message) + Nil then tree diff --git a/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/Rules.mls b/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/Rules.mls new file mode 100644 index 0000000000..879701454f --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/Rules.mls @@ -0,0 +1,368 @@ +import "std/Predef.mls" +import "std/Option.mls" +import "std/Stack.mls" +import "std/MutMap.mls" +import "std/Iter.mls" +import "./Token.mls" +import "./Keywords.mls" +import "./Tree.mls" +import "./ParseRule.mls" + +open Predef +open Option +open Stack +open ParseRule { Choice, rule } +open Token { Curly, Square, Round } + +open Choice { keyword, reference, term, typeExpr, ident, typeVar, end } + +type Opt[A] = Some[A] | None + +module Rules with ... + +val syntaxKinds = MutMap.empty + +val extendedKinds = new Set() + +fun getRuleByKind(kind: Str) = syntaxKinds |> MutMap.get(kind) |> Option.unsafe.get + +fun define(name: Str)(...choices) = syntaxKinds |> MutMap.updateWith(name) of case + None then Some(rule(name, ...choices)) + Some(rule) then Some(rule.extendChoices(choices Iter.toStack())) + +fun idFirst(value, _) = value +fun idSecond(_, value) = value +fun someFirst(value, _) = Some(value) +fun listFirst(value, _) = value :: Nil +fun listLike(fields) = + let mkTail = if fields.("sep") is undefined then id else keyword(fields.("sep")) + reference(fields.head) of + process: Cons, name: "the first " + fields.name + choices: tuple of end(Nil), mkTail of reference(fields.tail) of + process: idFirst, name: "more " + fields.name + "s" + +define("let-bindings") of term of + process: (lhs, rhsBindings) => if rhsBindings is [rhs, bindings] then + Tree.Infix(Keywords._equal, lhs, rhs) :: bindings + name: "left-hand side" + choices: tuple of keyword(Keywords._equal) of term of + name: "right-hand side" + choices: tuple of + end of Nil + keyword(Keywords._and) of reference("let-bindings") of + process: idFirst, name: "let-bindings tail" + +fun makeLetBindings(hasInClause: Bool) = + let intro = "let binding: " + keyword(Keywords._let) of Choice.siding of + optional: true + init: keyword(Keywords._rec)() + rest: reference("let-bindings") of + process: Tree.LetIn, name: "let-bindings" + choices: + if hasInClause then tuple of + keyword(Keywords._in) of term of + process: someFirst, name: intro + "body" + end of None + else tuple of end of None + process: idSecond + +let letExpression = makeLetBindings(true) + +define("simple-matching") of term of + process: (lhs, rhsTail) => if rhsTail is + [rhs, tail] then Tree.Infix(Keywords._thinArrow, lhs, rhs) :: tail + // TODO: Better error handling? + + name: "case body" + choices: tuple of keyword(Keywords._thinArrow) of term of + name: "rhs" + choices: tuple of + end of Nil + keyword(Keywords._bar) of reference("simple-matching") of + process: idFirst, name: "simple-matching tail" + +define("pattern-list") of term of + process: (head, tail) => head :: tail + name: "pattern" + choices: tuple of reference("pattern-list") of + process: idFirst, name: "pattern list tail" + +define("multiple-matching") of reference("pattern-list") of + process: Tree.infix of Keywords._thinArrow + name: "list of patterns" + choices: tuple of keyword(Keywords._thinArrow) of term of + process: idFirst, name: "the right-hand side of the arrow" + choices: tuple of + end of Nil + keyword(Keywords._bar) of reference("multiple-matching") of + process: idFirst, name: "multiple-matching tail" + +fun makeInfixChoice(kw: Keywords.Keyword, rhsKind: Str, compose: (Tree, Tree) -> Tree) = + keyword(kw) of reference(rhsKind) of + process: (rhs, _) => lhs => compose(lhs, rhs) + name: "operator `" + kw.name + "` right-hand side" + +fun makeBracketRule(fields) = // opening, closing, kind, wrapContent + // Pass the error message of closing bracket to the content. + keyword(fields.opening) of reference(fields.kind) of + process: (tree: Tree, next: Tree) => if next is + Tree.Error(Tree.Empty, msg) then fields.wrapContent(tree) Tree.Error(msg) + Tree.Empty then fields.wrapContent(tree) + name: fields.kind + " in bracket" + choices: tuple of keyword(fields.closing) of end of Tree.empty + +// Prefix rules and infix rules for expressions. +val termRule = rule of + "prefix rules for expressions" + letExpression + // `fun` term + keyword(Keywords._fun) of term of + process: (params, body) => Tree.Lambda(params :: Nil, body) + name: "function parameters" + choices: tuple of keyword(Keywords._thinArrow) of term of + process: idFirst, name: "function body" + // `match`-`with` term + keyword(Keywords._match) of term of + process: Tree.Match + name: "pattern matching scrutinee" + choices: tuple of keyword(Keywords._with) of Choice.siding of + optional: true + init: keyword(Keywords._bar)() + rest: getRuleByKind("simple-matching") + process: idSecond + // `function` term + keyword(Keywords._function) of Choice.siding of + optional: true + init: keyword(Keywords._bar)() + rest: getRuleByKind("simple-matching") + process: (_, branches) => Tree.Match(Tree.empty, branches) + // `if`-`then`-`else` term + keyword(Keywords._if) of term of + process: (tst, conAlt) => if conAlt is [con, alt] then + Tree.Ternary(Keywords._if, tst, con, alt) + name: "if-then-else condition" + choices: tuple of keyword(Keywords._then) of term of + name: "if-then-else consequent" + choices: tuple of + end of None + keyword(Keywords._else) of term of + process: someFirst, name: "if-then-else alternative" + // `while` term + keyword(Keywords._while) of term of + process: Tree.While + name: "while body" + choices: tuple of keyword(Keywords._do) of term of + name: "while end", process: idFirst + choices: tuple of keyword(Keywords._done)() + // `for` term + keyword(Keywords._for) of term of + name: "`for` head" + process: (head, startEndBody) => Tree.For(head, ...startEndBody) + choices: tuple of keyword(Keywords._equal) of term of + process: (start, endBody) => [start, ...endBody] + name: "`for` `to` or `downto` keyword" + choices: tuple of Choice.siding of + init: tuple of keyword(Keywords._to)(), keyword(Keywords._downto)() + rest: term of + name: "`for` `do` keyword" + choices: tuple of keyword(Keywords._do) of term of + name: "`for` `done` keyword", process: idFirst + choices: tuple of keyword(Keywords._done)() + process: idSecond + // Choices for brackets + makeBracketRule of + opening: Keywords._leftRound, closing: Keywords._rightRound, kind: "term" + wrapContent: (tree) => if tree is Tree.Empty then Tree.Tuple(Nil) else tree + makeBracketRule of + opening: Keywords._leftSquare, closing: Keywords._rightSquare, kind: "term" + wrapContent: (tree) => Tree.Bracketed of Square, if tree is + Tree.Empty then Tree.Sequence(Nil) + else tree + makeBracketRule of + opening: Keywords._leftCurly, closing: Keywords._rightCurly + kind: "term", wrapContent: id + makeBracketRule of + opening: Keywords._begin, closing: Keywords._end, kind: "term" + wrapContent: (tree) => if tree is Tree.Empty then Tree.Sequence(Nil) else tree + // "infix" rule starting with + term of + process: pipeInto + choices: tuple of + // Tuple (separated by commas) + makeInfixChoice of Keywords._comma, "term", (lhs, rhs) => if rhs is + Tree.Tuple(tail) then Tree.Tuple(lhs :: tail) + else Tree.Tuple(lhs :: rhs :: Nil) + // Sequence (separated by semicolons) + makeInfixChoice of Keywords._semicolon, "term", (lhs, rhs) => if rhs is + Tree.Sequence(tail) then Tree.Sequence(lhs :: tail) + else Tree.Sequence(lhs :: rhs :: Nil) + // Assignment: <- + makeInfixChoice of Keywords._leftArrow, "term", (lhs, rhs) => + Tree.Infix(Keywords._leftArrow, lhs, rhs) + // Comparison: == + makeInfixChoice of Keywords._equalequal, "term", (lhs, rhs) => + Tree.Infix(Keywords._equalequal, lhs, rhs) + // Comparison: * + makeInfixChoice of Keywords._asterisk, "term", (lhs, rhs) => + Tree.App(Tree.Ident("*", true), lhs :: rhs :: Nil) + // Selection: "." + // Access: "." "(" ")" + keyword(Keywords._period) of + keyword(Keywords._leftRound) of term of + process: (argument, _) => lhs => + Tree.Infix(Keywords._period, lhs, Tree.Bracketed(Round, argument)) + name: "application argument" + choices: tuple of keyword(Keywords._rightRound)() + term of + process: (rhs, _) => lhs => Tree.Infix(Keywords._period, lhs, rhs) + name: "operator `.` right-hand side" + // Type ascription: : + keyword(Keywords._colon) of typeExpr of + process: (rhs, _) => lhs => Tree.Infix(Keywords._colon, lhs, rhs) + name: "right-hand side type" + // Application: + term of + process: (argument, _) => callee => Tree.App(callee, argument) + name: "application argument" + outerPrec: Keywords.appPrec + +// Prefix rules and infix rules for types. +val typeRule = rule of + "rules for types" + // "(" { "," } ")" [ ] + keyword(Keywords._leftRound) of typeExpr of + process: (headArg, tailArgsCtor) => if tailArgsCtor is + [tailArgs, ctor] then Tree.App(ctor, Tree.Tuple(headArg :: tailArgs)) + Some(ctor) then Tree.App(ctor, headArg) + None then headArg + name: "the first type in the parentheses" + choices: tuple of + reference("type-arguments-tail") of + name: "the remaining type arguments" + choices: tuple of keyword(Keywords._rightRound) of + ident of process: someFirst, name: "the type constructor's name" + keyword(Keywords._rightRound) of // either an identifier or nothing + end of None + ident of process: someFirst, name: "the type constructor's name" + // "infix" rule starting with + typeExpr of + process: pipeInto + choices: tuple of + // "->" + makeInfixChoice of Keywords._thinArrow, "type", (lhs, rhs) => + Tree.Infix(Keywords._thinArrow, lhs, rhs) + // "*" + makeInfixChoice of Keywords._asterisk, "type", (lhs, rhs) => + Tree.Infix(Keywords._asterisk, lhs, rhs) + // Application: + typeExpr of + process: (callee, _) => argument => Tree.App(callee, argument) + outerPrec: Keywords.appPrec + +define("type-arguments-tail") of keyword(Keywords._comma) of listLike of + head: "type", tail: "type-arguments-tail", name: "type argument" + +define("constr-decl") of ident of + process: (ctor, argOpt) => if argOpt is + Some(arg) then Tree.Infix(Keywords._of, ctor, arg) + None then ctor + name: "the variant constructor's name" + choices: tuple of + end of None + keyword(Keywords._of) of typeExpr of + process: someFirst, name: "the variant constructor's argument" + +define("variants") of reference("constr-decl") of + process: (lhs, rhsOpt) => if rhsOpt is + Some(rhs) then Tree.Infix(Keywords._bar, lhs, rhs) + else lhs + name: "variants item" + choices: tuple of + end of None + keyword(Keywords._bar) of reference("variants") of + process: someFirst, name: "variants end" + +define("typedefs") of reference("typedef-lhs") of + process: (lhs, rhsMore) => if rhsMore is [rhs, more] then rhs(lhs) :: more + name: "typedef name" + choices: tuple of reference("typedef-rhs") of + name: "typedef body" + choices: tuple of + end of Nil + keyword(Keywords._and) of reference("typedefs") of + process: idFirst, name: "typedef end" + +define("typedef-rhs") of keyword(Keywords._equal) of + reference("variants") of + process: (rhs, _) => lhs => Tree.Infix(Keywords._equal, lhs, rhs) + name: "typedef-rhs: variants" + Choice.map of + keyword(Keywords._leftCurly) of + reference("label-decls") of + process: (content, _) => if content is + Nil then Tree.Bracketed(Curly, Tree.Sequence(Nil)) + else Tree.Bracketed(Curly, Tree.Sequence(content)) + name: "label-decl" + choices: keyword(Keywords._rightCurly) of end of Tree.empty + rhs => lhs => Tree.Infix(Keywords._equal, lhs, rhs) + +define("typedef-rhs") of keyword(Keywords._equalequal) of typeExpr of + process: (rhs, _) => lhs => Tree.Infix(Keywords._equalequal, lhs, rhs) + name: "type alias body" + +define("label-decl") of typeExpr of + process: Tree.infix of Keywords._colon + name: "label-decl name" + choices: tuple of keyword(Keywords._colon) of typeExpr of + process: idFirst, name: "label-decl body" + +define("label-decls") of listLike of + head: "label-decl", tail: "label-decls", + name: "label and declaration pair", sep: Keywords._semicolon + +define("constr-decls") of listLike of + head: "constr-decl", tail: "constr-decls", + name: "constructor declaration", sep: Keywords._bar + +define("typedef-lhs") of reference("type-params") of + process: (params, ident) => if params is + Nil then ident + else Tree.App(ident, Tree.Tuple(params)) + name: "the type parameters" + choices: tuple of ident of process: idFirst, name: "the type identifier" + +define("type-params") of end(Nil) +define("type-params") of typeVar of process: (h, _) => h :: Nil, name: "the only type parameter" +define("type-params") of keyword(Keywords._leftRound) of typeVar of + process: Cons + name: "the first type parameter" + choices: tuple of reference("type-params-tail") of + process: idFirst, name: "more type parameters" + choices: tuple of keyword(Keywords._rightRound)() + +define("type-params-tail") of end(Nil) +define("type-params-tail") of keyword(Keywords._comma) of typeVar of + process: Cons, name: "the first type parameter" + choices: tuple of reference("type-params-tail") of + process: idFirst, name: "more type parameters" + +val declRule = rule of + "prefix rules for module items" + makeLetBindings(false) // let definition + keyword(Keywords._type) of + reference("typedefs") of + process: (typedefs, _) => Tree.Define(Tree.DefineKind.Type, typedefs) + name: "more typedefs" + keyword(Keywords._exception) of + reference("constr-decls") of + process: (decls, _) => Tree.Define(Tree.DefineKind.Exception, decls) + name: "constructor declarations" + keyword(Keywords._hash) of ident of + process: (ident, body) => Tree.Define(Tree.DefineKind.Directive, [ident, body] :: Nil) + name: "directive name" + choices: tuple of term of process: idFirst, name: "directive body" + +syntaxKinds |> MutMap.insert of "term", termRule +syntaxKinds |> MutMap.insert of "type", typeRule +syntaxKinds |> MutMap.insert of "decl", declRule diff --git a/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/Token.mls b/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/Token.mls new file mode 100644 index 0000000000..56692f25b4 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/Token.mls @@ -0,0 +1,115 @@ +import "std/Option.mls" +import "std/Predef.mls" + +open Option { Some, None } +open Predef { mkStr } + +module Token with ... + +object + Angle + Round + Square + Curly + BeginEnd + +type BracketKind = Angle | Round | Square | Curly | BeginEnd + +module LiteralKind with + object + Integer + Decimal + String + Boolean + +abstract class Token with + let _location = None + fun withLocation(start, end, lookupTable) = + set _location = Some of start: start, end: end, lookupTable: lookupTable + this + fun location = _location + + fun displayLocation = if _location is + Some(location) then + let start = location.lookupTable.lookup(location.start) + let end = location.lookupTable.lookup(location.end) + mkStr of + start.0.toString(), ":", start.1.toString() + "-" + end.0.toString(), ":", end.1.toString() + else "" + +class LineLookupTable(lines: Array[Int]) with + fun lookup(index: Int): [Int, Int] = + if index < 0 do + set index = 0 + let + begin = 0 + end = lines.length + mid = Math.floor of (begin + end) / 2 + while begin < end do + if index <= lines.at(mid) then + set end = mid + else + set begin = mid + 1 + set mid = Math.floor of (begin + end) / 2 + let line = mid + 1 + let column = index - if mid == 0 then -1 else lines.at(mid - 1) + [line, column] + +data + class + Space() extends Token + Error() extends Token + Comment(content: Str) extends Token + Identifier(name: Str, symbolic: Bool) extends Token + Literal(kind, literal: Str) extends Token + +fun same(a, b) = if + a is Space and b is Space then true + a is Comment(c) and b is Comment(c') then c == c' + a is Identifier(n, s) and b is Identifier(n', s') then n == n' and s == s' + a is Literal(k, l) and b is Literal(k', l') then k == k' and l == l' + else false + +fun integer(literal, endIndex)(using llt: LineLookupTable) = + Literal(LiteralKind.Integer, literal).withLocation of + endIndex - literal.length, endIndex, llt +fun decimal(literal, endIndex)(using llt: LineLookupTable) = + Literal(LiteralKind.Decimal, literal).withLocation of + endIndex - literal.length, endIndex, llt +fun string(literal, startIndex, endIndex)(using llt: LineLookupTable) = + Literal(LiteralKind.String, literal).withLocation(startIndex, endIndex, llt) +fun boolean(literal, endIndex)(using llt: LineLookupTable) = + Literal(LiteralKind.Boolean, literal).withLocation of + endIndex - literal.length, endIndex, llt +fun identifier(name, endIndex)(using llt: LineLookupTable) = + Identifier(name, false).withLocation of + endIndex - name.length, endIndex, llt +fun symbol(name, endIndex)(using llt: LineLookupTable) = + Identifier(name, true).withLocation of + endIndex - name.length, endIndex, llt +fun comment(content, startIndex, endIndex)(using llt: LineLookupTable) = + Comment(content).withLocation(startIndex, endIndex, llt) +fun error(startIndex, endIndex)(using llt: LineLookupTable) = + Error().withLocation(startIndex, endIndex, llt) +fun space(startIndex, endIndex)(using llt: LineLookupTable) = + Space().withLocation(startIndex, endIndex, llt) + +fun summary(token) = if token is + Space then "␠" + Error then "⚠" + Comment(_) then "\uD83D\uDCAC" // The text ballon emoji. + Identifier(name, _) then name + Literal(_, literal) then literal + +fun display(token) = (if token is + Space then "space" + Error then "error" + Comment then "comment" + Identifier(name, _) then "identifier `" + name + "`" + Literal(kind, value) then mkStr of + kind.toString().toLowerCase() + " " + JSON.stringify(value) + ) + " at " + token.displayLocation diff --git a/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/TokenHelpers.mls b/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/TokenHelpers.mls new file mode 100644 index 0000000000..368d01ab7f --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/TokenHelpers.mls @@ -0,0 +1,28 @@ +import "./Token.mls" +import "std/Stack.mls" +import "std/Predef.mls" + +open Stack +open Predef { mkStr } + +type StackT[A] = Stack.Cons[A] | Stack.Nil + +module TokenHelpers with ... + +fun display(tokens: StackT[Token.Token], limit: Int): Str = + let + i = 0 + values = mut [] + while i < limit and tokens is head :: tail do + values.push of head Token.summary() + set + tokens = tail + i += 1 + mkStr of + "┃" + values.join("β”‚"), + if tokens is _ :: _ then "β”‚β‹―" else "┃" + +fun panorama(tokens) = display(tokens, Number.MAX_SAFE_INTEGER) + +fun preview(tokens) = display(tokens, 5) diff --git a/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/Tree.mls b/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/Tree.mls new file mode 100644 index 0000000000..2f86aeb392 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/Tree.mls @@ -0,0 +1,205 @@ +import "std/Iter.mls" +import "std/Predef.mls" +import "std/Stack.mls" +import "std/Option.mls" +import "std/StrOps.mls" +import "./Keywords.mls" +import "./Token.mls" + +open Predef { fold, mkStr } +open Stack +open Option { Some, None } +open Token { LiteralKind, BracketKind } +open Keywords { opPrec, INT_MAX } + +// _____ +// |_ _| __ ___ ___ +// | || '__/ _ \/ _ \ +// | || | | __/ __/ +// |_||_| \___|\___| +// +// ==================== + +abstract class Tree +module Tree with + + module DefineKind with + data class Let(recursive: Bool) + object Type + object Exception + object Directive + + type DefineKind = Let | Type | Exception | Directive + + open DefineKind + + data + class + Empty() extends Tree + Error(tree: Tree, message: Str) extends Tree + Bracketed(kind: BracketKind, tree: Tree) extends Tree + Ident(name: Str, symbolic: Bool) extends Tree + Underscore() extends Tree + Modified(modifier, subject) extends Tree + Tuple(trees: Stack[Tree]) extends Tree + Sequence(trees: Stack[Tree]) extends Tree + Literal(kind, value) extends Tree + Match(scrutinee: Tree, branches: Stack[Tree]) extends Tree + Lambda(params: Stack[Tree], body: Tree) extends Tree + App(callee: Tree, argument: Tree) extends Tree + Infix(op: Keywords.Keyword, lhs: Tree, rhs: Tree) extends Tree + // `items` is separated by `and` + Define(kind: DefineKind.DefineKind, items: Stack[[Tree, Tree]]) extends Tree + LetIn(bindings: Stack[Tree], body: Tree) extends Tree + While(cond: Tree, body: Tree) extends Tree + For(head: Tree, start: Tree, end: Tree, body: Tree) extends Tree + // For `if`-`then`-`else`. The last part is optional. + Ternary(keyword: Keywords.Keyword, lhs: Tree, rhs: Tree, body) extends Tree + + fun empty = Empty() + fun error(message: Str) = empty Error(message) + fun summary(tree) = + fun par(text: Str, cond: Bool) = if cond then "(" + text + ")" else text + fun prec(tree: Tree, side: Bool) = if tree is + Empty then INT_MAX + Error(tree, _) then prec(tree, side) + Bracketed(_, _) then INT_MAX + Ident then INT_MAX + Underscore then INT_MAX + Modified then 1 + Tuple then INT_MAX + Sequence then 1 + Literal then INT_MAX + Match then 2 + App(callee, _) and callee is + Ident(op, true) and opPrec(op) is [leftPrec, rightPrec] and + side then rightPrec + else leftPrec + else Keywords.appPrec + Infix(op, _, _) and + side then op.rightPrecOrMax + else op.leftPrecOrMax + Ternary then 3 + Lambda then Keywords._fun.leftPrecOrMax + // Wrap trees in guillemet so they are easier to spot. + fun wrap(something) = if something is + Tree then "Β«" + go(something) + "Β»" + else go(something) + fun go(tree) = if tree is + Empty then "{}" + Error(Empty, _) then "⚠" + Error(tree, _) then "<⚠:" + go(tree) + ">" + Bracketed(kind, tree) and kind is + Token.Round then "(" + go(tree) + ")" + Token.Square then "[" + go(tree) + "]" + Token.Curly then "{" + go(tree) + "}" + Token.Angle then "<" + go(tree) + ">" + Ident(name, _) then name + Underscore() then "_" + Modified(modifier, subject) then go(modifier) + " " + go(subject) + Tuple(trees) then "(" + trees Iter.fromStack() Iter.mapping(go) Iter.joined(", ") + ")" + Sequence(trees) then trees Iter.fromStack() Iter.mapping(go) Iter.joined("; ") + Literal(LiteralKind.String, value) and + value.length > 5 then JSON.stringify(value.slice(0, 5)).slice(0, -1) + "…\"" + else JSON.stringify(value) + Literal(_, value) then value + Match(scrutinee, branches) then mkStr of + if scrutinee is Empty then "function " else + "match " + go(scrutinee) + " with " + branches Iter.fromStack() Iter.mapping(go) Iter.joined(" | ") + // Function application for binary operators. + App(Ident(op, true), lhs :: rhs :: Nil) then + if opPrec(op) is [leftPrec, rightPrec] then fold(+) of + par of go(lhs), prec(lhs, false) < leftPrec + " ", op, " " + par of go(rhs), prec(rhs, true) < rightPrec + // Unary expressions + App(Ident(op, true), arg) then mkStr of + op + par of go(arg), prec(arg, false) <= Keywords.prefixPrec + // Function application + App(callee, argument) then mkStr of + go(callee) + " " + par of go(argument), prec(argument, false) <= Keywords.appPrec + Infix(Keywords.Keyword(".", _, _), target, Ident(field, _)) then + if opPrec(".") is [leftPrec, _] then mkStr of + par of go(target), prec(target, false) < leftPrec + "." + field + Infix(op, lhs, rhs) then fold(+) of + go(lhs), " ", go(op), " ", go(rhs) + Define(Directive, [name, value] :: Nil) then StrOps.concat of + "#", go(name), " ", go(value) + Define(kind, items) then mkStr of + if kind is + Let(true) then "let rec " + Let(false) then "let " + Type then "type " + Exception then "exception " + items + Iter.fromStack() + Iter.mapping of case + Tree as tree then go(tree) + [lhs, rhs] then go(lhs) + " = " + go(rhs) + Iter.joined(" and ") + LetIn(bindings, body) then mkStr of + "let " + bindings + Iter.fromStack() + Iter.mapping of go + Iter.joined(" and ") + ...if body is + Some(body) then [" in ", go(body)] + None then [] + While(cond, body) then mkStr of "while ", go(cond), " do ", go(body), " done" + For(head, start, end, body) then mkStr of + "for ", go(head), " = ", go(start), " to ", go(end), " do ", go(body), " done" + Ternary(keyword, lhs, rhs, body) then fold(+) of + keyword.name, " ", go(lhs), + if keyword.name is + "if" then " then " + "type" then " = " + "let" then " = " + if rhs is Some(rhs') then go(rhs') else go(rhs) + if keyword.name is + "if" then " then " + "type" then "" + "let" then " in " + if body is Some(body) then go(body) else go(body) + Lambda(params, body) then fold(+) of + "fun ", params Iter.fromStack() Iter.mapping(go) Iter.joined(" "), " -> ", go(body) + Keywords.Keyword(name, _, _) then name + Some(tree) then "Some(" + wrap(tree) + ")" + None then "None" + _ :: _ then tree Iter.fromStack() Iter.mapping(wrap) Iter.joined(" :: ") + " :: Nil" + Nil then "Nil" + [..trees] then "[" + trees Iter.mapping((tree, _, _) => wrap(tree)) Iter.joined(", ") + "]" + else "" + wrap(tree) + + fun infix(op)(lhs, rhs) = Infix(op, lhs, rhs) + + fun bracketed(tree, kind) = Bracketed(kind, tree) + + fun asSequence(tree) = if tree is + Empty then Sequence of Nil + Sequence then tree + else Sequence of tree :: Nil + + fun tupleWithHead(tree, head) = if tree is + Tuple(tail) then Tuple(head :: tail) + else Tuple(head :: tree :: Nil) + + fun sequenceWithHead(tree, head) = if tree is + Sequence(tail) then Sequence(head :: tail) + else Sequence(head :: tree :: Nil) + + fun nonEmpty(tree) = if tree is + Empty then false + Error(Empty, _) then false + else true + + fun nonEmptyError(tree) = if tree is + Error(Empty, _) then false + else true diff --git a/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/TreeHelpers.mls b/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/TreeHelpers.mls new file mode 100644 index 0000000000..43eb33d72c --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/TreeHelpers.mls @@ -0,0 +1,76 @@ +import "std/Option.mls" +import "std/Predef.mls" +import "std/Stack.mls" +import "./Tree.mls" +import "./Keywords.mls" +import "./Token.mls" + +open Predef +open Stack +open Token { LiteralKind } +open Option { Some, None } + +module TreeHelpers with ... + +fun first(array) = if array is [first, ...] then first + +fun second(array) = if array is [_, second, ...] then second + +fun indented(text) = text.split("\n").join("\n ") + +fun showAsTree(thing) = + fun itemize(something) = if something is + Some(content) then tuple of ["Some of " + go(content)], [] + None then tuple of "None", [] + head :: tail then + let + items = mut [go(head)] + remaining = tail + while remaining is + head' :: tail' do + items.push(go of head') + set remaining = tail' + tuple of ("Stack of \n" + " " + indented of items.join("\n")), [] + Nil then ["Nil", []] + Str then [JSON.stringify(something), []] // TODO: This doesn't work. + Int then [something.toString(), []] + Tree.Empty then ["Empty", []] + Tree.Error(Tree.Empty, m) then tuple of "Error", [["message", go(m)]] + Tree.Error(t, m) then tuple of "Error", [["tree", go(t)], ["message", go(m)]] + Tree.Ident(n, _) then tuple of "Ident", [["name", go(n)]] + Tree.Bracketed(k, items) then tuple of + "Bracketed#" + k.toString(), [["items", go(items)]] + Tree.Underscore() then tuple of "Underscore", [] + Tree.Modified(m, s) then + tuple of "Modified", [["modifier", go(m)], ["subject", go(s)]] + Tree.Tuple(t) then tuple of "Tuple", [["items", go(t)]] + Tree.Sequence(t) then tuple of "Sequence", [["items", go(t)]] + Tree.Literal(k, v) then tuple of ("Literal#" + go(k) + " of " + go(v)), [] + Tree.Match(scrutinee, branches) then tuple of + "Match", [["scrutinee", scrutinee], ["branches", go(branches)]] + Tree.App(c, a) then tuple of "App", [["callee", go(c)], ["argument", go(a)]] + Tree.Infix(op, lhs, rhs) then tuple of + "Infix", [["op", go(op)], ["lhs", go(lhs)], ["rhs", go(rhs)]] + Tree.Define(k, i) then tuple of "Define", [["kind", k.toString()], ["items", go(i)]] + Tree.LetIn(bds, b) then tuple of "LetIn", [["bindings", go(bds)], ["body", go(b)]] + Tree.While(c, b) then tuple of "While", [["condition", go(c)], ["body", go(b)]] + Tree.For(h, s, e, b) then tuple of + "For", [["head", go(h)], ["start", go(s)], ["end", go(e)], ["body", go(b)]] + Tree.Ternary(n, l, r, b) then tuple of + "Ternary", [["name", go(n)], ["lhs", go(l)], ["rhs", go(r)], ["body", go(b)]] + Tree.Lambda(p, b) then tuple of "Lambda", [["params", go(p)], ["body", go(b)]] + Keywords.Keyword as keyword then [keyword.toString(), []] + LiteralKind.Integer then tuple of "Integer", [] + LiteralKind.Decimal then tuple of "Decimal", [] + LiteralKind.String then tuple of "String", [] + LiteralKind.Boolean then tuple of "Boolean", [] + [x, y] then tuple of "Pair", [["first", go(x)], ["second", go(y)]] + else tuple of "Unknown", [["JSON.stringify(_)", JSON.stringify(something)]] + fun go(something) = if itemize(something) is + [intro, []] then intro + [intro, [field]] and intro != "Unknown" then intro + " of " + second of field + [intro, fields] then + let dialogue = fields.map of (field, _, _) => + field first() + " = " + field second() + intro + ":\n " + indented of dialogue.join("\n") + go(thing) diff --git a/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/manifest.json b/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/manifest.json new file mode 100644 index 0000000000..9ef1ed0f09 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/manifest.json @@ -0,0 +1,22 @@ +{ + "name": "generalized-pratt-parsing", + "main": "Parser.mls", + "moduleName": "Parser", + "vendors": [ + { + "prefix": "std/", + "path": "../../mlscript-compile", + "files": [ + "Predef.mls", + "Char.mls", + "Stack.mls", + "StrOps.mls", + "Option.mls", + "Iter.mls", + "MutMap.mls", + "TreeTracer.mls", + "XML.mls" + ] + } + ] +} diff --git a/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/thirdparty/railroad/railroad.css b/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/thirdparty/railroad/railroad.css new file mode 100644 index 0000000000..00ca93fcdc --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/thirdparty/railroad/railroad.css @@ -0,0 +1,52 @@ +svg.railroad-diagram { + /* background-color: hsl(30,20%,95%); */ + background: transparent; +} +svg.railroad-diagram path { + stroke-width: 2; + stroke: rgba(1, 1, 1, 0.80); + fill: transparent; +} +svg.railroad-diagram text { + text-anchor: middle; + white-space: pre; +} +svg.railroad-diagram text.diagram-text { + font-size: 12px; +} +svg.railroad-diagram text.diagram-arrow { + font-size: 16px; +} +svg.railroad-diagram text.label { + text-anchor: start; +} +svg.railroad-diagram text.comment { + font: italic 12px monospace; +} +svg.railroad-diagram g.non-terminal text { + font-style: -apple-system, system-ui, sans-serif; + font-weight: 600; + font-style: italic; +} +svg.railroad-diagram g.terminal text { + font-family: Inconsolata, monospace; +} +svg.railroad-diagram rect { + stroke-width: 1.5; + stroke: black; + fill: #ffffff; +} +svg.railroad-diagram rect.group-box { + stroke: gray; + stroke-dasharray: 10 5; + fill: none; +} +svg.railroad-diagram path.diagram-text { + stroke-width: 1.5; + stroke: black; + fill: white; + cursor: help; +} +svg.railroad-diagram g.diagram-text:hover path.diagram-text { + fill: #eee; +} diff --git a/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/thirdparty/railroad/railroad.mjs b/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/thirdparty/railroad/railroad.mjs new file mode 100644 index 0000000000..3f0b24ebc3 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/generalized-pratt-parsing/thirdparty/railroad/railroad.mjs @@ -0,0 +1,2467 @@ +"use strict"; +/* +Railroad Diagrams +by Tab Atkins Jr. (and others) +http://xanthir.com +http://twitter.com/tabatkins +http://github.com/tabatkins/railroad-diagrams + +This document and all associated files in the github project are licensed under CC0: http://creativecommons.org/publicdomain/zero/1.0/ +This means you can reuse, remix, or otherwise appropriate this project for your own use WITHOUT RESTRICTION. +(The actual legal meaning can be found at the above link.) +Don't ask me for permission to use any part of this project, JUST USE IT. +I would appreciate attribution, but that is not required by the license. +*/ + +// Export function versions of all the constructors. +// Each class will add itself to this object. +const funcs = {}; +export default funcs; + +export const Options = { + DEBUG: false, // if true, writes some debug information into attributes + VS: 8, // minimum vertical separation between things. For a 3px stroke, must be at least 4 + AR: 10, // radius of arcs + DIAGRAM_CLASS: 'railroad-diagram', // class to put on the root + STROKE_ODD_PIXEL_LENGTH: true, // is the stroke width an odd (1px, 3px, etc) pixel length? + INTERNAL_ALIGNMENT: 'center', // how to align items when they have extra space. left/right/center + CHAR_WIDTH: 8.5, // width of each monospace character. play until you find the right value for your font + COMMENT_CHAR_WIDTH: 7, // comments are in smaller text by default + ESCAPE_HTML: true, // Should Diagram.toText() produce HTML-escaped text, or raw? +}; + +export const defaultCSS = ` + svg { + background-color: hsl(30,20%,95%); + } + path { + stroke-width: 3; + stroke: black; + fill: rgba(0,0,0,0); + } + text { + font: bold 14px monospace; + text-anchor: middle; + white-space: pre; + } + text.diagram-text { + font-size: 12px; + } + text.diagram-arrow { + font-size: 16px; + } + text.label { + text-anchor: start; + } + text.comment { + font: italic 12px monospace; + } + g.non-terminal text { + /*font-style: italic;*/ + } + rect { + stroke-width: 3; + stroke: black; + fill: hsl(120,100%,90%); + } + rect.group-box { + stroke: gray; + stroke-dasharray: 10 5; + fill: none; + } + path.diagram-text { + stroke-width: 3; + stroke: black; + fill: white; + cursor: help; + } + g.diagram-text:hover path.diagram-text { + fill: #eee; + }`; + + +export class FakeSVG { + constructor(tagName, attrs, text) { + if(text) this.children = text; + else this.children = []; + this.tagName = tagName; + this.attrs = unnull(attrs, {}); + } + format(x, y, width) { + // Virtual + } + addTo(parent) { + if(parent instanceof FakeSVG) { + parent.children.push(this); + return this; + } else { + var svg = this.toSVG(); + parent.appendChild(svg); + return svg; + } + } + toSVG() { + var el = SVG(this.tagName, this.attrs); + if(typeof this.children == 'string') { + el.textContent = this.children; + } else { + this.children.forEach(function(e) { + el.appendChild(e.toSVG()); + }); + } + return el; + } + toString() { + var str = '<' + this.tagName; + var group = this.tagName == "g" || this.tagName == "svg"; + for(var attr in this.attrs) { + str += ' ' + attr + '="' + (this.attrs[attr]+'').replace(/&/g, '&').replace(/"/g, '"') + '"'; + } + str += '>'; + if(group) str += "\n"; + if(typeof this.children == 'string') { + str += escapeString(this.children); + } else { + this.children.forEach(function(e) { + str += e; + }); + } + str += '\n'; + return str; + } + toTextDiagram() { + return new TextDiagram(0, 0, []); + } + toText() { + var outputTD = this.toTextDiagram(); + var output = outputTD.lines.join("\n") + "\n"; + if(Options.ESCAPE_HTML) { + output = output.replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """); + } + return output; + } + walk(cb) { + cb(this); + } +} + + +export class Path extends FakeSVG { + constructor(x,y) { + super('path'); + this.attrs.d = "M"+x+' '+y; + } + m(x,y) { + this.attrs.d += 'm'+x+' '+y; + return this; + } + h(val) { + this.attrs.d += 'h'+val; + return this; + } + right(val) { return this.h(Math.max(0, val)); } + left(val) { return this.h(-Math.max(0, val)); } + v(val) { + this.attrs.d += 'v'+val; + return this; + } + down(val) { return this.v(Math.max(0, val)); } + up(val) { return this.v(-Math.max(0, val)); } + arc(sweep){ + // 1/4 of a circle + var x = Options.AR; + var y = Options.AR; + if(sweep[0] == 'e' || sweep[1] == 'w') { + x *= -1; + } + if(sweep[0] == 's' || sweep[1] == 'n') { + y *= -1; + } + var cw; + if(sweep == 'ne' || sweep == 'es' || sweep == 'sw' || sweep == 'wn') { + cw = 1; + } else { + cw = 0; + } + this.attrs.d += "a"+Options.AR+" "+Options.AR+" 0 0 "+cw+' '+x+' '+y; + return this; + } + arc_8(start, dir) { + // 1/8 of a circle + const arc = Options.AR; + const s2 = 1/Math.sqrt(2) * arc; + const s2inv = (arc - s2); + let path = "a " + arc + " " + arc + " 0 0 " + (dir=='cw' ? "1" : "0") + " "; + const sd = start+dir; + const offset = + sd == 'ncw' ? [s2, s2inv] : + sd == 'necw' ? [s2inv, s2] : + sd == 'ecw' ? [-s2inv, s2] : + sd == 'secw' ? [-s2, s2inv] : + sd == 'scw' ? [-s2, -s2inv] : + sd == 'swcw' ? [-s2inv, -s2] : + sd == 'wcw' ? [s2inv, -s2] : + sd == 'nwcw' ? [s2, -s2inv] : + sd == 'nccw' ? [-s2, s2inv] : + sd == 'nwccw' ? [-s2inv, s2] : + sd == 'wccw' ? [s2inv, s2] : + sd == 'swccw' ? [s2, s2inv] : + sd == 'sccw' ? [s2, -s2inv] : + sd == 'seccw' ? [s2inv, -s2] : + sd == 'eccw' ? [-s2inv, -s2] : + sd == 'neccw' ? [-s2, -s2inv] : null + ; + path += offset.join(" "); + this.attrs.d += path; + return this; + } + l(x, y) { + this.attrs.d += 'l'+x+' '+y; + return this; + } + format() { + // All paths in this library start/end horizontally. + // The extra .5 ensures a minor overlap, so there's no seams in bad rasterizers. + this.attrs.d += 'h.5'; + return this; + } + toTextDiagram() { + return new TextDiagram(0, 0, []); + } +} + + +export class DiagramMultiContainer extends FakeSVG { + constructor(tagName, items, attrs, text) { + super(tagName, attrs, text); + this.items = items.map(wrapString); + } + walk(cb) { + cb(this); + this.items.forEach(x=>x.walk(cb)); + } +} + + +export class Diagram extends DiagramMultiContainer { + constructor(...items) { + super('svg', items, {class: Options.DIAGRAM_CLASS}); + if(!(this.items[0] instanceof Start)) { + this.items.unshift(new Start()); + } + if(!(this.items[this.items.length-1] instanceof End)) { + this.items.push(new End()); + } + this.up = this.down = this.height = this.width = 0; + for(const item of this.items) { + this.width += item.width + (item.needsSpace?20:0); + this.up = Math.max(this.up, item.up - this.height); + this.height += item.height; + this.down = Math.max(this.down - item.height, item.down); + } + this.formatted = false; + } + format(paddingt, paddingr, paddingb, paddingl) { + paddingt = unnull(paddingt, 20); + paddingr = unnull(paddingr, paddingt, 20); + paddingb = unnull(paddingb, paddingt, 20); + paddingl = unnull(paddingl, paddingr, 20); + var x = paddingl; + var y = paddingt; + y += this.up; + var g = new FakeSVG('g', Options.STROKE_ODD_PIXEL_LENGTH ? {transform:'translate(.5 .5)'} : {}); + for(var i = 0; i < this.items.length; i++) { + var item = this.items[i]; + if(item.needsSpace) { + new Path(x,y).h(10).addTo(g); + x += 10; + } + item.format(x, y, item.width).addTo(g); + x += item.width; + y += item.height; + if(item.needsSpace) { + new Path(x,y).h(10).addTo(g); + x += 10; + } + } + this.attrs.width = this.width + paddingl + paddingr; + this.attrs.height = this.up + this.height + this.down + paddingt + paddingb; + this.attrs.viewBox = "0 0 " + this.attrs.width + " " + this.attrs.height; + g.addTo(this); + this.formatted = true; + return this; + } + addTo(parent) { + if(!parent) { + var scriptTag = document.getElementsByTagName('script'); + scriptTag = scriptTag[scriptTag.length - 1]; + parent = scriptTag.parentNode; + } + return super.addTo.call(this, parent); + } + toSVG() { + if(!this.formatted) { + this.format(); + } + return super.toSVG.call(this); + } + toString() { + if(!this.formatted) { + this.format(); + } + return super.toString.call(this); + } + toStandalone(style) { + if(!this.formatted) { + this.format(); + } + const s = new FakeSVG('style', {}, style || defaultCSS); + this.children.push(s); + this.attrs.xmlns = "http://www.w3.org/2000/svg"; + this.attrs['xmlns:xlink'] = "http://www.w3.org/1999/xlink"; + const result = super.toString.call(this); + this.children.pop(); + delete this.attrs.xmlns; + return result; + } + toTextDiagram() { + var [separator] = TextDiagram._getParts(["separator"]) + var diagramTD = this.items[0].toTextDiagram(); + for(const item of this.items.slice(1)) { + var itemTD = item.toTextDiagram(); + if(item.needsSpace) { + itemTD = itemTD.expand(1, 1, 0, 0); + } + diagramTD = diagramTD.appendRight(itemTD, separator); + } + return diagramTD; + } +} +funcs.Diagram = (...args)=>new Diagram(...args); + + +export class ComplexDiagram extends FakeSVG { + constructor(...items) { + var diagram = new Diagram(...items); + diagram.items[0] = new Start({type:"complex"}); + diagram.items[diagram.items.length-1] = new End({type:"complex"}); + return diagram; + } +} +funcs.ComplexDiagram = (...args)=>new ComplexDiagram(...args); + + +export class Sequence extends DiagramMultiContainer { + constructor(...items) { + super('g', items); + var numberOfItems = this.items.length; + this.needsSpace = true; + this.up = this.down = this.height = this.width = 0; + for(var i = 0; i < this.items.length; i++) { + var item = this.items[i]; + this.width += item.width + (item.needsSpace?20:0); + this.up = Math.max(this.up, item.up - this.height); + this.height += item.height; + this.down = Math.max(this.down - item.height, item.down); + } + if(this.items[0].needsSpace) this.width -= 10; + if(this.items[this.items.length-1].needsSpace) this.width -= 10; + if(Options.DEBUG) { + this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; + this.attrs['data-type'] = "sequence"; + } + } + format(x,y,width) { + // Hook up the two sides if this is narrower than its stated width. + var gaps = determineGaps(width, this.width); + new Path(x,y).h(gaps[0]).addTo(this); + new Path(x+gaps[0]+this.width,y+this.height).h(gaps[1]).addTo(this); + x += gaps[0]; + + for(var i = 0; i < this.items.length; i++) { + var item = this.items[i]; + if(item.needsSpace && i > 0) { + new Path(x,y).h(10).addTo(this); + x += 10; + } + item.format(x, y, item.width).addTo(this); + x += item.width; + y += item.height; + if(item.needsSpace && i < this.items.length-1) { + new Path(x,y).h(10).addTo(this); + x += 10; + } + } + return this; + } + toTextDiagram() { + var [separator] = TextDiagram._getParts(["separator"]) + var diagramTD = new TextDiagram(0, 0, [""]) + for(const item of this.items) { + var itemTD = item.toTextDiagram(); + if(item.needsSpace) { + itemTD = itemTD.expand(1, 1, 0, 0); + } + diagramTD = diagramTD.appendRight(itemTD, separator); + } + return diagramTD; + } +} +funcs.Sequence = (...args)=>new Sequence(...args); + + +export class Stack extends DiagramMultiContainer { + constructor(...items) { + super('g', items); + if( items.length === 0 ) { + throw new RangeError("Stack() must have at least one child."); + } + this.width = Math.max.apply(null, this.items.map(function(e) { return e.width + (e.needsSpace?20:0); })); + //if(this.items[0].needsSpace) this.width -= 10; + //if(this.items[this.items.length-1].needsSpace) this.width -= 10; + if(this.items.length > 1){ + this.width += Options.AR*2; + } + this.needsSpace = true; + this.up = this.items[0].up; + this.down = this.items[this.items.length-1].down; + + this.height = 0; + var last = this.items.length - 1; + for(var i = 0; i < this.items.length; i++) { + var item = this.items[i]; + this.height += item.height; + if(i > 0) { + this.height += Math.max(Options.AR*2, item.up + Options.VS); + } + if(i < last) { + this.height += Math.max(Options.AR*2, item.down + Options.VS); + } + } + if(Options.DEBUG) { + this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; + this.attrs['data-type'] = "stack"; + } + } + format(x,y,width) { + var gaps = determineGaps(width, this.width); + new Path(x,y).h(gaps[0]).addTo(this); + x += gaps[0]; + var xInitial = x; + if(this.items.length > 1) { + new Path(x, y).h(Options.AR).addTo(this); + x += Options.AR; + } + + for(var i = 0; i < this.items.length; i++) { + var item = this.items[i]; + var innerWidth = this.width - (this.items.length>1 ? Options.AR*2 : 0); + item.format(x, y, innerWidth).addTo(this); + x += innerWidth; + y += item.height; + + if(i !== this.items.length-1) { + new Path(x, y) + .arc('ne').down(Math.max(0, item.down + Options.VS - Options.AR*2)) + .arc('es').left(innerWidth) + .arc('nw').down(Math.max(0, this.items[i+1].up + Options.VS - Options.AR*2)) + .arc('ws').addTo(this); + y += Math.max(item.down + Options.VS, Options.AR*2) + Math.max(this.items[i+1].up + Options.VS, Options.AR*2); + //y += Math.max(Options.AR*4, item.down + Options.VS*2 + this.items[i+1].up) + x = xInitial+Options.AR; + } + + } + + if(this.items.length > 1) { + new Path(x,y).h(Options.AR).addTo(this); + x += Options.AR; + } + new Path(x,y).h(gaps[1]).addTo(this); + + return this; + } + toTextDiagram() { + var [corner_bot_left, corner_bot_right, corner_top_left, corner_top_right, line, line_vertical] = TextDiagram._getParts(["corner_bot_left", "corner_bot_right", "corner_top_left", "corner_top_right", "line", "line_vertical"]); + + // Format all the child items, so we can know the maximum width. + var itemTDs = []; + for(const item of this.items) { + itemTDs.push(item.toTextDiagram()); + } + var maxWidth = Math.max(...(itemTDs.map(function(itemTD) {return itemTD.width;}))); + + var leftLines = []; + var rightLines = []; + var separatorTD = new TextDiagram(0, 0, [line.repeat(maxWidth)]); + var diagramTD = null; // Top item will replace it + + for(var [itemNum, itemTD] of enumerate(itemTDs)) { + if(itemNum == 0) { + // The top item enters directly from its left. + leftLines.push(line + line); + for(var i = 0; i < (itemTD.height - itemTD.entry - 1); i++) { + leftLines.push(" "); + } + } else { + // All items below the top enter from a snake-line from the previous item's exit. + // Here, we resume that line, already having descended from above on the right. + diagramTD = diagramTD.appendBelow(separatorTD, []); + leftLines.push(corner_top_left + line); + for(i = 0; i < itemTD.entry; i++) { + leftLines.push(line_vertical + " ") + } + leftLines.push(corner_bot_left + line); + for(i = 0; i < (itemTD.height - itemTD.entry - 1); i++) { + leftLines.push(" "); + } + for(i = 0; i < itemTD.exit; i++) { + rightLines.push(" "); + } + } + if(itemNum < itemTDs.length - 1) { + // All items above the bottom exit via a snake-line to the next item's entry. + // Here, we start that line on the right. + rightLines.push(line + corner_top_right); + for(i = 0; i < (itemTD.height - itemTD.exit - 1); i++) { + rightLines.push(" " + line_vertical); + } + rightLines.push(line + corner_bot_right); + } else { + // The bottom item exits directly to its right. + rightLines.push(line + line); + } + var [leftPad, rightPad] = TextDiagram._gaps(maxWidth, itemTD.width); + itemTD = itemTD.expand(leftPad, rightPad, 0, 0); + if(itemNum == 0) { + diagramTD = itemTD; + } else { + diagramTD = diagramTD.appendBelow(itemTD, []); + } + } + + var leftTD = new TextDiagram(0, 0, leftLines); + diagramTD = leftTD.appendRight(diagramTD, ""); + var rightTD = new TextDiagram(0, rightLines.length - 1, rightLines); + diagramTD = diagramTD.appendRight(rightTD, ""); + return diagramTD; + } +} +funcs.Stack = (...args)=>new Stack(...args); + + +export class OptionalSequence extends DiagramMultiContainer { + constructor(...items) { + super('g', items); + if( items.length === 0 ) { + throw new RangeError("OptionalSequence() must have at least one child."); + } + if( items.length === 1 ) { + return new Sequence(items); + } + var arc = Options.AR; + this.needsSpace = false; + this.width = 0; + this.up = 0; + this.height = sum(this.items, function(x){return x.height}); + this.down = this.items[0].down; + var heightSoFar = 0; + for(var i = 0; i < this.items.length; i++) { + var item = this.items[i]; + this.up = Math.max(this.up, Math.max(arc*2, item.up + Options.VS) - heightSoFar); + heightSoFar += item.height; + if(i > 0) { + this.down = Math.max(this.height + this.down, heightSoFar + Math.max(arc*2, item.down + Options.VS)) - this.height; + } + var itemWidth = (item.needsSpace?10:0) + item.width; + if(i === 0) { + this.width += arc + Math.max(itemWidth, arc); + } else { + this.width += arc*2 + Math.max(itemWidth, arc) + arc; + } + } + if(Options.DEBUG) { + this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; + this.attrs['data-type'] = "optseq"; + } + } + format(x, y, width) { + var arc = Options.AR; + var gaps = determineGaps(width, this.width); + new Path(x, y).right(gaps[0]).addTo(this); + new Path(x + gaps[0] + this.width, y + this.height).right(gaps[1]).addTo(this); + x += gaps[0]; + var upperLineY = y - this.up; + var last = this.items.length - 1; + for(var i = 0; i < this.items.length; i++) { + var item = this.items[i]; + var itemSpace = (item.needsSpace?10:0); + var itemWidth = item.width + itemSpace; + if(i === 0) { + // Upper skip + new Path(x,y) + .arc('se') + .up(y - upperLineY - arc*2) + .arc('wn') + .right(itemWidth - arc) + .arc('ne') + .down(y + item.height - upperLineY - arc*2) + .arc('ws') + .addTo(this); + // Straight line + new Path(x, y) + .right(itemSpace + arc) + .addTo(this); + item.format(x + itemSpace + arc, y, item.width).addTo(this); + x += itemWidth + arc; + y += item.height; + // x ends on the far side of the first element, + // where the next element's skip needs to begin + } else if(i < last) { + // Upper skip + new Path(x, upperLineY) + .right(arc*2 + Math.max(itemWidth, arc) + arc) + .arc('ne') + .down(y - upperLineY + item.height - arc*2) + .arc('ws') + .addTo(this); + // Straight line + new Path(x,y) + .right(arc*2) + .addTo(this); + item.format(x + arc*2, y, item.width).addTo(this); + new Path(x + item.width + arc*2, y + item.height) + .right(itemSpace + arc) + .addTo(this); + // Lower skip + new Path(x,y) + .arc('ne') + .down(item.height + Math.max(item.down + Options.VS, arc*2) - arc*2) + .arc('ws') + .right(itemWidth - arc) + .arc('se') + .up(item.down + Options.VS - arc*2) + .arc('wn') + .addTo(this); + x += arc*2 + Math.max(itemWidth, arc) + arc; + y += item.height; + } else { + // Straight line + new Path(x, y) + .right(arc*2) + .addTo(this); + item.format(x + arc*2, y, item.width).addTo(this); + new Path(x + arc*2 + item.width, y + item.height) + .right(itemSpace + arc) + .addTo(this); + // Lower skip + new Path(x,y) + .arc('ne') + .down(item.height + Math.max(item.down + Options.VS, arc*2) - arc*2) + .arc('ws') + .right(itemWidth - arc) + .arc('se') + .up(item.down + Options.VS - arc*2) + .arc('wn') + .addTo(this); + } + } + return this; + } + toTextDiagram() { + var [line, line_vertical, roundcorner_bot_left, roundcorner_bot_right, roundcorner_top_left, roundcorner_top_right] = TextDiagram._getParts(["line", "line_vertical", "roundcorner_bot_left", "roundcorner_bot_right", "roundcorner_top_left", "roundcorner_top_right"]); + + // Format all the child items, so we can know the maximum entry. + var itemTDs = []; + for(const item of this.items) { + itemTDs.push(item.toTextDiagram()); + } + // diagramEntry: distance from top to lowest entry, aka distance from top to diagram entry, aka final diagram entry and exit. + var diagramEntry = Math.max(...(itemTDs.map(function(itemTD) {return itemTD.entry}))); + // SOILHeight: distance from top to lowest entry before rightmost item, aka distance from skip-over-items line to rightmost entry, aka SOIL height. + var SOILHeight = Math.max(...(itemTDs.slice(0,-1).map(function(itemTD) {return itemTD.entry;}))); + // topToSOIL: distance from top to skip-over-items line. + var topToSOIL = diagramEntry - SOILHeight; + + // The diagram starts with a line from its entry up to the skip-over-items line { + var lines = []; + for(var i = 0; i < topToSOIL; i++) { + lines.push(" "); + } + lines.push(roundcorner_top_left + line); + for(i = 0; i < SOILHeight; i++) { + lines.push(line_vertical + " "); + } + lines.push(roundcorner_bot_right + line); + var diagramTD = new TextDiagram(lines.length - 1, lines.length - 1, lines); + for(const [itemNum, itemTD] of enumerate(itemTDs)) { + if(itemNum > 0) { + // All items except the leftmost start with a line from their entry down to their skip-under-item line, + // with a joining-line across at the skip-over-items line + lines = []; + for(i = 0; i < topToSOIL; i++) { + lines.push(" "); + } + lines.push(line + line); + for(i = 0; i < (diagramTD.exit - topToSOIL - 1); i++) { + lines.push(" "); + } + lines.push(line + roundcorner_top_right); + for(i = 0; i < (itemTD.height - itemTD.entry - 1); i++) { + lines.push(" " + line_vertical); + } + lines.push(" " + roundcorner_bot_left); + var skipDownTD = new TextDiagram(diagramTD.exit, diagramTD.exit, lines); + diagramTD = diagramTD.appendRight(skipDownTD, ""); + // All items except the leftmost next have a line from skip-over-items line down to their entry, + // with joining-lines at their entry and at their skip-under-item line + lines = []; + for(i = 0; i < topToSOIL; i++) { + lines.push(" "); + } + // All such items except the rightmost also have a continuation of the skip-over-items line + var lineToNextItem = itemNum < itemTDs.length - 1 ? line : " "; + lines.push(line + roundcorner_top_right + lineToNextItem); + for(i = 0; i < (diagramTD.exit - topToSOIL - 1); i++) { + lines.push(" " + line_vertical + " "); + } + lines.push(line + roundcorner_bot_left + line); + for(i = 0; i < (itemTD.height - itemTD.entry - 1); i++) { + lines.push(" "); + } + lines.push(line + line + line); + var entryTD = new TextDiagram(diagramTD.exit, diagramTD.exit, lines); + diagramTD = diagramTD.appendRight(entryTD, ""); + } + var partTD = new TextDiagram(0, 0, []); + if(itemNum < itemTDs.length - 1) { + // All items except the rightmost have a segment of the skip-over-items line at the top, + // followed by enough blank lines to push their entry down to the previous item's exit + lines = []; + lines.push(line.repeat(itemTD.width)); + for(i = 0; i < (SOILHeight - itemTD.entry); i++) { + lines.push(" ".repeat(itemTD.width)); + } + var SOILSegment = new TextDiagram(0, 0, lines); + partTD = partTD.appendBelow(SOILSegment, []); + } + partTD = partTD.appendBelow(itemTD, [], true, true); + if (itemNum > 0) { + // All items except the leftmost have their skip-under-item line at the bottom. + var SUILSegment = new TextDiagram(0, 0, [line.repeat(itemTD.width)]); + partTD = partTD.appendBelow(SUILSegment, []); + } + diagramTD = diagramTD.appendRight(partTD, ""); + if(0 < itemNum) { + // All items except the leftmost have a line from their skip-under-item line to their exit + lines = []; + for(i = 0; i < topToSOIL; i++) { + lines.push(" "); + } + // All such items except the rightmost also have a joining-line across at the skip-over-items line + var skipOverChar = itemNum < itemTDs.length - 1 ? line : " "; + lines.push(skipOverChar.repeat(2)); + for(i = 0; i < (diagramTD.exit - topToSOIL - 1); i++) { + lines.push(" "); + } + lines.push(line + roundcorner_top_left); + for(i = 0; i < (partTD.height - partTD.exit - 2); i++) { + lines.push(" " + line_vertical); + } + lines.push(line + roundcorner_bot_right); + var skipUpTD = new TextDiagram(diagramTD.exit, diagramTD.exit, lines); + diagramTD = diagramTD.appendRight(skipUpTD, ""); + } + } + return diagramTD; + } +} +funcs.OptionalSequence = (...args)=>new OptionalSequence(...args); + + +export class AlternatingSequence extends DiagramMultiContainer { + constructor(...items) { + super('g', items); + if( items.length === 1 ) { + return new Sequence(items); + } + if( items.length !== 2 ) { + throw new RangeError("AlternatingSequence() must have one or two children."); + } + this.needsSpace = false; + + const arc = Options.AR; + const vert = Options.VS; + const max = Math.max; + const first = this.items[0]; + const second = this.items[1]; + + const arcX = 1 / Math.sqrt(2) * arc * 2; + const arcY = (1 - 1 / Math.sqrt(2)) * arc * 2; + const crossY = Math.max(arc, Options.VS); + const crossX = (crossY - arcY) + arcX; + + const firstOut = max(arc + arc, crossY/2 + arc + arc, crossY/2 + vert + first.down); + this.up = firstOut + first.height + first.up; + + const secondIn = max(arc + arc, crossY/2 + arc + arc, crossY/2 + vert + second.up); + this.down = secondIn + second.height + second.down; + + this.height = 0; + + const firstWidth = 2*(first.needsSpace?10:0) + first.width; + const secondWidth = 2*(second.needsSpace?10:0) + second.width; + this.width = 2*arc + max(firstWidth, crossX, secondWidth) + 2*arc; + + if(Options.DEBUG) { + this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; + this.attrs['data-type'] = "altseq"; + } + } + format(x, y, width) { + const arc = Options.AR; + const gaps = determineGaps(width, this.width); + new Path(x,y).right(gaps[0]).addTo(this); + x += gaps[0]; + new Path(x+this.width, y).right(gaps[1]).addTo(this); + // bounding box + //new Path(x+gaps[0], y).up(this.up).right(this.width).down(this.up+this.down).left(this.width).up(this.down).addTo(this); + const first = this.items[0]; + const second = this.items[1]; + + // top + const firstIn = this.up - first.up; + const firstOut = this.up - first.up - first.height; + new Path(x,y).arc('se').up(firstIn-2*arc).arc('wn').addTo(this); + first.format(x + 2*arc, y - firstIn, this.width - 4*arc).addTo(this); + new Path(x + this.width - 2*arc, y - firstOut).arc('ne').down(firstOut - 2*arc).arc('ws').addTo(this); + + // bottom + const secondIn = this.down - second.down - second.height; + const secondOut = this.down - second.down; + new Path(x,y).arc('ne').down(secondIn - 2*arc).arc('ws').addTo(this); + second.format(x + 2*arc, y + secondIn, this.width - 4*arc).addTo(this); + new Path(x + this.width - 2*arc, y + secondOut).arc('se').up(secondOut - 2*arc).arc('wn').addTo(this); + + // crossover + const arcX = 1 / Math.sqrt(2) * arc * 2; + const arcY = (1 - 1 / Math.sqrt(2)) * arc * 2; + const crossY = Math.max(arc, Options.VS); + const crossX = (crossY - arcY) + arcX; + const crossBar = (this.width - 4*arc - crossX)/2; + new Path(x+arc, y - crossY/2 - arc).arc('ws').right(crossBar) + .arc_8('n', 'cw').l(crossX - arcX, crossY - arcY).arc_8('sw', 'ccw') + .right(crossBar).arc('ne').addTo(this); + new Path(x+arc, y + crossY/2 + arc).arc('wn').right(crossBar) + .arc_8('s', 'ccw').l(crossX - arcX, -(crossY - arcY)).arc_8('nw', 'cw') + .right(crossBar).arc('se').addTo(this); + + return this; + } + toTextDiagram() { + var [cross_diag, corner_bot_left, corner_bot_right, corner_top_left, corner_top_right, line, line_vertical, tee_left, tee_right] = TextDiagram._getParts(["cross_diag", "roundcorner_bot_left", "roundcorner_bot_right", "roundcorner_top_left", "roundcorner_top_right", "line", "line_vertical", "tee_left", "tee_right"]); + + var firstTD = this.items[0].toTextDiagram(); + var secondTD = this.items[1].toTextDiagram(); + var maxWidth = TextDiagram._maxWidth(firstTD, secondTD); + var [leftWidth, rightWidth] = TextDiagram._gaps(maxWidth, 0); + var leftLines = []; + var rightLines = []; + var separator = []; + var [leftSize, rightSize] = TextDiagram._gaps(firstTD.width, 0); + var diagramTD = firstTD.expand(leftWidth - leftSize, rightWidth - rightSize, 0, 0); + for(var i = 0; i < diagramTD.entry; i++) { + leftLines.push(" "); + } + leftLines.push(corner_top_left + line); + for(i = 0; i < (diagramTD.height - diagramTD.entry - 1); i++) { + leftLines.push(line_vertical + " "); + } + leftLines.push(corner_bot_left + line); + for(i = 0; i < diagramTD.exit; i++) { + rightLines.push(" "); + } + rightLines.push(line + corner_top_right); + for(i = 0; i < (diagramTD.height - diagramTD.exit - 1); i++) { + rightLines.push(" " + line_vertical); + } + rightLines.push(line + corner_bot_right); + + separator.push((line.repeat(leftWidth - 1)) + corner_top_right + " " + corner_top_left + (line.repeat(rightWidth - 2))); + separator.push((" ".repeat(leftWidth - 1)) + " " + cross_diag + " " + (" ".repeat(rightWidth - 2))); + separator.push((line.repeat(leftWidth - 1)) + corner_bot_right + " " + corner_bot_left + (line.repeat(rightWidth - 2))); + leftLines.push(" "); + rightLines.push(" "); + + [leftSize, rightSize] = TextDiagram._gaps(secondTD.width, 0); + secondTD = secondTD.expand(leftWidth - leftSize, rightWidth - rightSize, 0, 0); + diagramTD = diagramTD.appendBelow(secondTD, separator, true, true); + leftLines.push(corner_top_left + line); + for(i = 0; i < secondTD.entry; i++) { + leftLines.push(line_vertical + " "); + } + leftLines.push(corner_bot_left + line); + rightLines.push(line + corner_top_right); + for(i = 0; i < secondTD.exit; i++) { + rightLines.push(" " + line_vertical); + } + rightLines.push(line + corner_bot_right); + + diagramTD = diagramTD.alter(firstTD.height + Math.trunc(separator.length / 2), firstTD.height + Math.trunc(separator.length / 2)); + var leftTD = new TextDiagram(firstTD.height + Math.trunc(separator.length / 2), firstTD.height + Math.trunc(separator.length / 2), leftLines); + var rightTD = new TextDiagram(firstTD.height + Math.trunc(separator.length / 2), firstTD.height + Math.trunc(separator.length / 2), rightLines); + diagramTD = leftTD.appendRight(diagramTD, "").appendRight(rightTD, ""); + diagramTD = new TextDiagram(1, 1, [corner_top_left, tee_left, corner_bot_left]).appendRight(diagramTD, "").appendRight(new TextDiagram(1, 1, [corner_top_right, tee_right, corner_bot_right]), ""); + return diagramTD; + } +} +funcs.AlternatingSequence = (...args)=>new AlternatingSequence(...args); + + +export class Choice extends DiagramMultiContainer { + constructor(normal, ...items) { + super('g', items); + if( typeof normal !== "number" || normal !== Math.floor(normal) ) { + throw new TypeError("The first argument of Choice() must be an integer."); + } else if(normal < 0 || normal >= items.length) { + throw new RangeError("The first argument of Choice() must be an index for one of the items."); + } else { + this.normal = normal; + } + this.width = max(this.items, el=>el.width) + Options.AR*4; + var firstItem = this.items[0]; + var lastItem = this.items[items.length - 1]; + var normalItem = this.items[normal]; + + // The size of the vertical separation between an item + // and the following item. + // The calcs are non-trivial and need to be done both here + // and in .format(), so no reason to do it twice. + this.separators = Array.from({length:items.length-1}, x=>0); + + // If the entry or exit lines would be too close together + // to accommodate the arcs, + // bump up the vertical separation to compensate. + this.up = 0 + var arcs; + for(var i = normal - 1; i >= 0; i--) { + if(i == normal-1) arcs = Options.AR * 2; + else arcs = Options.AR; + + let item = this.items[i]; + let lowerItem = this.items[i+1]; + + let entryDelta = lowerItem.up + Options.VS + item.down + item.height; + let exitDelta = lowerItem.height + lowerItem.up + Options.VS + item.down; + + let separator = Options.VS; + if(exitDelta < arcs || entryDelta < arcs) { + separator += Math.max(arcs - entryDelta, arcs - exitDelta) + } + this.separators[i] = separator + this.up += lowerItem.up + separator + item.down + item.height + } + this.up += firstItem.up; + + this.height = normalItem.height; + + this.down = 0; + for(var i = normal+1; i < this.items.length; i++) { + if(i == normal+1) arcs = Options.AR * 2; + else arcs = Options.AR; + + let item = this.items[i]; + let upperItem = this.items[i-1]; + + let entryDelta = upperItem.height + upperItem.down + Options.VS + item.up; + let exitDelta = upperItem.down + Options.VS + item.up + item.height; + + let separator = Options.VS; + if(entryDelta < arcs || exitDelta < arcs) { + separator += Math.max(arcs - entryDelta, arcs - exitDelta) + } + this.separators[i-1] = separator; + this.down += upperItem.down + separator + item.up + item.height; + } + this.down += lastItem.down; + + if(Options.DEBUG) { + this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; + this.attrs['data-type'] = "choice"; + } + } + format(x,y,width) { + // Hook up the two sides if this is narrower than its stated width. + var gaps = determineGaps(width, this.width); + new Path(x,y).h(gaps[0]).addTo(this); + new Path(x+gaps[0]+this.width,y+this.height).h(gaps[1]).addTo(this); + x += gaps[0]; + + var last = this.items.length -1; + var innerWidth = this.width - Options.AR*4; + + // Do the elements that curve above + var distanceFromY = 0; + for(var i = this.normal - 1; i >= 0; i--) { + let item = this.items[i]; + let lowerItem = this.items[i+1]; + distanceFromY += lowerItem.up + this.separators[i] + item.down + item.height; + new Path(x,y) + .arc('se') + .up(distanceFromY - Options.AR*2) + .arc('wn').addTo(this); + item.format(x+Options.AR*2,y - distanceFromY,innerWidth).addTo(this); + new Path(x+Options.AR*2+innerWidth, y-distanceFromY+item.height) + .arc('ne') + .down(distanceFromY - item.height + this.height - Options.AR*2) + .arc('ws').addTo(this); + } + + // Do the straight-line path. + new Path(x,y).right(Options.AR*2).addTo(this); + this.items[this.normal].format(x+Options.AR*2, y, innerWidth).addTo(this); + new Path(x+Options.AR*2+innerWidth, y+this.height).right(Options.AR*2).addTo(this); + + // Do the elements that curve below + var distanceFromY = 0; + for(var i = this.normal+1; i <= last; i++) { + let item = this.items[i]; + let upperItem = this.items[i-1]; + distanceFromY += upperItem.height + upperItem.down + this.separators[i-1] + item.up; + new Path(x,y) + .arc('ne') + .down(distanceFromY - Options.AR*2) + .arc('ws').addTo(this); + if(!item.format) console.log(item); + item.format(x+Options.AR*2, y+distanceFromY, innerWidth).addTo(this); + new Path(x+Options.AR*2+innerWidth, y+distanceFromY+item.height) + .arc('se') + .up(distanceFromY - Options.AR*2 + item.height - this.height) + .arc('wn').addTo(this); + } + + return this; + } + toTextDiagram() { + var [cross, line, line_vertical, roundcorner_bot_left, roundcorner_bot_right, roundcorner_top_left, roundcorner_top_right] = TextDiagram._getParts(["cross", "line", "line_vertical", "roundcorner_bot_left", "roundcorner_bot_right", "roundcorner_top_left", "roundcorner_top_right"]); + // Format all the child items, so we can know the maximum width. + var itemTDs = []; + for(const item of this.items) { + itemTDs.push(item.toTextDiagram().expand(1, 1, 0, 0)); + } + var max_item_width = Math.max(...(itemTDs.map(function(itemTD) {return itemTD.width}))); + var diagramTD = new TextDiagram(0, 0, []); + // Format the choice collection. + for(var [itemNum, itemTD] of enumerate(itemTDs)) { + var [leftPad, rightPad] = TextDiagram._gaps(max_item_width, itemTD.width); + itemTD = itemTD.expand(leftPad, rightPad, 0, 0); + var hasSeparator = true; + var leftLines = []; + var rightLines = []; + for (var i = 0; i < itemTD.height; i++) { + leftLines.push(line_vertical); + rightLines.push(line_vertical); + } + var moveEntry = false; + var moveExit = false; + if(itemNum <= this.normal) { + // Item above the line: round off the entry/exit lines upwards. + leftLines[itemTD.entry] = roundcorner_top_left; + rightLines[itemTD.exit] = roundcorner_top_right; + if(itemNum == 0) { + // First item and above the line: also remove ascenders above the item's entry and exit, suppress the separator above it. + hasSeparator = false; + for(i = 0; i < itemTD.entry; i++) { + leftLines[i] = " "; + } + for(i = 0; i < itemTD.exit; i++) { + rightLines[i] = " "; + } + } + } + if(itemNum >= this.normal) { + // Item below the line: round off the entry/exit lines downwards. + leftLines[itemTD.entry] = roundcorner_bot_left; + rightLines[itemTD.exit] = roundcorner_bot_right; + if(itemNum == 0) { + // First item and below the line: also suppress the separator above it. + hasSeparator = false; + } + if(itemNum == (this.items.length - 1)) { + // Last item and below the line: also remove descenders below the item's entry and exit + for(i = itemTD.entry + 1; i < itemTD.height; i++) { + leftLines[i] = " "; + } + for(i = itemTD.exit + 1; i < itemTD.height; i++) { + rightLines[i] = " "; + } + } + } + if(itemNum == this.normal) { + // Item on the line: entry/exit are horizontal, and sets the outer entry/exit. + leftLines[itemTD.entry] = cross; + rightLines[itemTD.exit] = cross; + moveEntry = true; + moveExit = true; + if(itemNum == 0 && itemNum == (this.items.length - 1)) { + // Only item and on the line: set entry/exit for straight through. + leftLines[itemTD.entry] = line; + rightLines[itemTD.exit] = line; + } else if(itemNum == 0) { + // First item and on the line: set entry/exit for no ascenders. + leftLines[itemTD.entry] = roundcorner_top_right; + rightLines[itemTD.exit] = roundcorner_top_left; + } else if(itemNum == (this.items.length - 1)) { + // Last item and on the line: set entry/exit for no descenders. + leftLines[itemTD.entry] = roundcorner_bot_right; + rightLines[itemTD.exit] = roundcorner_bot_left; + } + } + var leftJointTD = new TextDiagram(itemTD.entry, itemTD.entry, leftLines); + var rightJointTD = new TextDiagram(itemTD.exit, itemTD.exit, rightLines); + itemTD = leftJointTD.appendRight(itemTD, "").appendRight(rightJointTD, ""); + var separator = hasSeparator ? [line_vertical + (" ".repeat(TextDiagram._maxWidth(diagramTD, itemTD) - 2)) + line_vertical] : []; + diagramTD = diagramTD.appendBelow(itemTD, separator, moveEntry, moveExit); + } + return diagramTD; + } +} +funcs.Choice = (...args)=>new Choice(...args); + + +export class HorizontalChoice extends DiagramMultiContainer { + constructor(...items) { + super('g', items); + if( items.length === 0 ) { + throw new RangeError("HorizontalChoice() must have at least one child."); + } + if( items.length === 1) { + return new Sequence(items); + } + const allButLast = this.items.slice(0, -1); + const middles = this.items.slice(1, -1); + const first = this.items[0]; + const last = this.items[this.items.length - 1]; + this.needsSpace = false; + + this.width = Options.AR; // starting track + this.width += Options.AR*2 * (this.items.length-1); // inbetween tracks + this.width += sum(this.items, x=>x.width + (x.needsSpace?20:0)); // items + this.width += (last.height > 0 ? Options.AR : 0); // needs space to curve up + this.width += Options.AR; //ending track + + // Always exits at entrance height + this.height = 0; + + // All but the last have a track running above them + this._upperTrack = Math.max( + Options.AR*2, + Options.VS, + max(allButLast, x=>x.up) + Options.VS + ); + this.up = Math.max(this._upperTrack, last.up); + + // All but the first have a track running below them + // Last either straight-lines or curves up, so has different calculation + this._lowerTrack = Math.max( + Options.VS, + max(middles, x=>x.height+Math.max(x.down+Options.VS, Options.AR*2)), + last.height + last.down + Options.VS + ); + if(first.height < this._lowerTrack) { + // Make sure there's at least 2*AR room between first exit and lower track + this._lowerTrack = Math.max(this._lowerTrack, first.height + Options.AR*2); + } + this.down = Math.max(this._lowerTrack, first.height + first.down); + + + if(Options.DEBUG) { + this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; + this.attrs['data-type'] = "horizontalchoice"; + } + } + format(x,y,width) { + // Hook up the two sides if this is narrower than its stated width. + var gaps = determineGaps(width, this.width); + new Path(x,y).h(gaps[0]).addTo(this); + new Path(x+gaps[0]+this.width,y+this.height).h(gaps[1]).addTo(this); + x += gaps[0]; + + const first = this.items[0]; + const last = this.items[this.items.length-1]; + const allButFirst = this.items.slice(1); + const allButLast = this.items.slice(0, -1); + + // upper track + var upperSpan = (sum(allButLast, x=>x.width+(x.needsSpace?20:0)) + + (this.items.length - 2) * Options.AR*2 + - Options.AR + ); + new Path(x,y) + .arc('se') + .v(-(this._upperTrack - Options.AR*2)) + .arc('wn') + .h(upperSpan) + .addTo(this); + + // lower track + var lowerSpan = (sum(allButFirst, x=>x.width+(x.needsSpace?20:0)) + + (this.items.length - 2) * Options.AR*2 + + (last.height > 0 ? Options.AR : 0) + - Options.AR + ); + var lowerStart = x + Options.AR + first.width+(first.needsSpace?20:0) + Options.AR*2; + new Path(lowerStart, y+this._lowerTrack) + .h(lowerSpan) + .arc('se') + .v(-(this._lowerTrack - Options.AR*2)) + .arc('wn') + .addTo(this); + + // Items + for(const [i, item] of enumerate(this.items)) { + // input track + if(i === 0) { + new Path(x,y) + .h(Options.AR) + .addTo(this); + x += Options.AR; + } else { + new Path(x, y - this._upperTrack) + .arc('ne') + .v(this._upperTrack - Options.AR*2) + .arc('ws') + .addTo(this); + x += Options.AR*2; + } + + // item + var itemWidth = item.width + (item.needsSpace?20:0); + item.format(x, y, itemWidth).addTo(this); + x += itemWidth; + + // output track + if(i === this.items.length-1) { + if(item.height === 0) { + new Path(x,y) + .h(Options.AR) + .addTo(this); + } else { + new Path(x,y+item.height) + .arc('se') + .addTo(this); + } + } else if(i === 0 && item.height > this._lowerTrack) { + // Needs to arc up to meet the lower track, not down. + if(item.height - this._lowerTrack >= Options.AR*2) { + new Path(x, y+item.height) + .arc('se') + .v(this._lowerTrack - item.height + Options.AR*2) + .arc('wn') + .addTo(this); + } else { + // Not enough space to fit two arcs + // so just bail and draw a straight line for now. + new Path(x, y+item.height) + .l(Options.AR*2, this._lowerTrack - item.height) + .addTo(this); + } + } else { + new Path(x, y+item.height) + .arc('ne') + .v(this._lowerTrack - item.height - Options.AR*2) + .arc('ws') + .addTo(this); + } + } + return this; + } + toTextDiagram() { + var [line, line_vertical, roundcorner_bot_left, roundcorner_bot_right, roundcorner_top_left, roundcorner_top_right] = TextDiagram._getParts(["line", "line_vertical", "roundcorner_bot_left", "roundcorner_bot_right", "roundcorner_top_left", "roundcorner_top_right"]); + + // Format all the child items, so we can know the maximum entry, exit, and height. + var itemTDs = []; + for(const item of this.items) { + itemTDs.push(item.toTextDiagram()); + } + // diagramEntry: distance from top to lowest entry, aka distance from top to diagram entry, aka final diagram entry and exit. + var diagramEntry = Math.max(...(itemTDs.map(function(itemTD) {return itemTD.entry}))); + // SOILToBaseline: distance from top to lowest entry before rightmost item, aka distance from skip-over-items line to rightmost entry, aka SOIL height. + var SOILToBaseline = Math.max(...(itemTDs.slice(0, -1).map(function(itemTD) {return itemTD.entry}))); + // topToSOIL: distance from top to skip-over-items line. + var topToSOIL = diagramEntry - SOILToBaseline; + // baselineToSUIL: distance from lowest entry or exit after leftmost item to bottom, aka distance from entry to skip-under-items line, aka SUIL height. + var baselineToSUIL = Math.max(...(itemTDs.slice(1).map(function(itemTD) {return itemTD.height - Math.min(itemTD.entry, itemTD.exit) - 1;}))); + + // The diagram starts with a line from its entry up to skip-over-items line { + var lines = []; + for(var i = 0; i < topToSOIL; i++) { + lines.push(" "); + } + lines.push(roundcorner_top_left + line); + for(i = 0; i < SOILToBaseline; i++) { + lines.push(line_vertical + " "); + } + lines.push(roundcorner_bot_right + line); + var diagramTD = new TextDiagram(lines.length - 1, lines.length - 1, lines); + for(const [itemNum, itemTD] of enumerate(itemTDs)) { + if(itemNum > 0) { + // All items except the leftmost start with a line from the skip-over-items line down to their entry, + // with a joining-line across at the skip-under-items line { + lines = []; + for(i = 0; i < topToSOIL; i++) { + lines.push(" "); + } + // All such items except the rightmost also have a continuation of the skip-over-items line { + var lineToNextItem = itemNum == itemTDs.length - 1 ? " " : line; + lines.push(roundcorner_top_right + lineToNextItem); + for(i = 0; i < SOILToBaseline; i++) { + lines.push(line_vertical + " "); + } + lines.push(roundcorner_bot_left + line); + for(i = 0; i < baselineToSUIL; i++) { + lines.push(line_vertical + " "); + } + lines.push(line + line); + var entryTD = new TextDiagram(diagramTD.exit, diagramTD.exit, lines); + diagramTD = diagramTD.appendRight(entryTD, ""); + } + var partTD = new TextDiagram(0, 0, []); + if(itemNum < itemTDs.length - 1) { + // All items except the rightmost start with a segment of the skip-over-items line at the top. + // followed by enough blank lines to push their entry down to the previous item's exit { + lines = []; + lines.push(line.repeat(itemTD.width)); + for(i = 0; i < (SOILToBaseline - itemTD.entry); i++) { + lines.push(" ".repeat(itemTD.width)); + } + var SOILSegment = new TextDiagram(0, 0, lines); + partTD = partTD.appendBelow(SOILSegment, []); + } + partTD = partTD.appendBelow(itemTD, [], true, true); + if(itemNum > 0) { + // All items except the leftmost end with enough blank lines to pad down to the skip-under-items + // line, followed by a segment of the skip-under-items line { + lines = []; + for(i = 0; i < (baselineToSUIL - (itemTD.height - itemTD.entry) + 1); i++) { + lines.push(" ".repeat(itemTD.width)); + } + lines.push(line.repeat(itemTD.width)); + var SUILSegment = new TextDiagram(0, 0, lines); + partTD = partTD.appendBelow(SUILSegment, []); + } + diagramTD = diagramTD.appendRight(partTD, ""); + if(itemNum < itemTDs.length - 1) { + // All items except the rightmost have a line from their exit down to the skip-under-items line, + // with a joining-line across at the skip-over-items line { + lines = []; + for(i = 0; i < topToSOIL; i++) { + lines.push(" "); + } + lines.push(line + line); + for(i = 0; i < (diagramTD.exit - topToSOIL - 1); i++) { + lines.push(" "); + } + lines.push(line + roundcorner_top_right); + for(i = 0; i < (baselineToSUIL - (diagramTD.exit - diagramTD.entry)); i++) { + lines.push(" " + line_vertical); + } + // All such items except the leftmost also have are continuing of the skip-under-items line from the previous item { + var lineFromPrevItem = itemNum > 0 ? line : " "; + lines.push(lineFromPrevItem + roundcorner_bot_left); + var entry = diagramEntry + 1 + (diagramTD.exit - diagramTD.entry); + var exitTD = new TextDiagram(entry, diagramEntry + 1, lines); + diagramTD = diagramTD.appendRight(exitTD, ""); + } else { + // The rightmost item has a line from the skip-under-items line and from its exit up to the diagram exit { + lines = []; + var lineFromExit = diagramTD.exit != diagramTD.entry ? " " : line; + lines.push(lineFromExit + roundcorner_top_left); + for(i = 0; i < (diagramTD.exit - diagramTD.entry - 1); i++) { + lines.push(" " + line_vertical); + } + if(diagramTD.exit != diagramTD.entry) { + lines.push(line + roundcorner_bot_right); + } + for(i = 0; i < (baselineToSUIL - (diagramTD.exit - diagramTD.entry)); i++) { + lines.push(" " + line_vertical); + } + lines.push(line + roundcorner_bot_right); + exitTD = new TextDiagram(diagramTD.exit - diagramTD.entry, 0, lines); + diagramTD = diagramTD.appendRight(exitTD, ""); + } + } + return diagramTD; + } +} +funcs.HorizontalChoice = (...args)=>new HorizontalChoice(...args); + + +export class MultipleChoice extends DiagramMultiContainer { + constructor(normal, type, ...items) { + super('g', items); + if( typeof normal !== "number" || normal !== Math.floor(normal) ) { + throw new TypeError("The first argument of MultipleChoice() must be an integer."); + } else if(normal < 0 || normal >= items.length) { + throw new RangeError("The first argument of MultipleChoice() must be an index for one of the items."); + } else { + this.normal = normal; + } + if( type != "any" && type != "all" ) { + throw new SyntaxError("The second argument of MultipleChoice must be 'any' or 'all'."); + } else { + this.type = type; + } + this.needsSpace = true; + this.innerWidth = max(this.items, function(x){return x.width}); + this.width = 30 + Options.AR + this.innerWidth + Options.AR + 20; + this.up = this.items[0].up; + this.down = this.items[this.items.length-1].down; + this.height = this.items[normal].height; + for(var i = 0; i < this.items.length; i++) { + let item = this.items[i]; + let minimum; + if(i == normal - 1 || i == normal + 1) minimum = 10 + Options.AR; + else minimum = Options.AR; + if(i < normal) { + this.up += Math.max(minimum, item.height + item.down + Options.VS + this.items[i+1].up); + } else if(i > normal) { + this.down += Math.max(minimum, item.up + Options.VS + this.items[i-1].down + this.items[i-1].height); + } + } + this.down -= this.items[normal].height; // already counted in this.height + if(Options.DEBUG) { + this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; + this.attrs['data-type'] = "multiplechoice"; + } + } + format(x, y, width) { + var gaps = determineGaps(width, this.width); + new Path(x, y).right(gaps[0]).addTo(this); + new Path(x + gaps[0] + this.width, y + this.height).right(gaps[1]).addTo(this); + x += gaps[0]; + + var normal = this.items[this.normal]; + + // Do the elements that curve above + var distanceFromY; + for(var i = this.normal - 1; i >= 0; i--) { + var item = this.items[i]; + if( i == this.normal - 1 ) { + distanceFromY = Math.max(10 + Options.AR, normal.up + Options.VS + item.down + item.height); + } + new Path(x + 30,y) + .up(distanceFromY - Options.AR) + .arc('wn').addTo(this); + item.format(x + 30 + Options.AR, y - distanceFromY, this.innerWidth).addTo(this); + new Path(x + 30 + Options.AR + this.innerWidth, y - distanceFromY + item.height) + .arc('ne') + .down(distanceFromY - item.height + this.height - Options.AR - 10) + .addTo(this); + if(i !== 0) { + distanceFromY += Math.max(Options.AR, item.up + Options.VS + this.items[i-1].down + this.items[i-1].height); + } + } + + new Path(x + 30, y).right(Options.AR).addTo(this); + normal.format(x + 30 + Options.AR, y, this.innerWidth).addTo(this); + new Path(x + 30 + Options.AR + this.innerWidth, y + this.height).right(Options.AR).addTo(this); + + for(i = this.normal+1; i < this.items.length; i++) { + let item = this.items[i]; + if(i == this.normal + 1) { + distanceFromY = Math.max(10+Options.AR, normal.height + normal.down + Options.VS + item.up); + } + new Path(x + 30, y) + .down(distanceFromY - Options.AR) + .arc('ws') + .addTo(this); + item.format(x + 30 + Options.AR, y + distanceFromY, this.innerWidth).addTo(this); + new Path(x + 30 + Options.AR + this.innerWidth, y + distanceFromY + item.height) + .arc('se') + .up(distanceFromY - Options.AR + item.height - normal.height) + .addTo(this); + if(i != this.items.length - 1) { + distanceFromY += Math.max(Options.AR, item.height + item.down + Options.VS + this.items[i+1].up); + } + } + var text = new FakeSVG('g', {"class": "diagram-text"}).addTo(this); + new FakeSVG('title', {}, (this.type=="any"?"take one or more branches, once each, in any order":"take all branches, once each, in any order")).addTo(text); + new FakeSVG('path', { + "d": "M "+(x+30)+" "+(y-10)+" h -26 a 4 4 0 0 0 -4 4 v 12 a 4 4 0 0 0 4 4 h 26 z", + "class": "diagram-text" + }).addTo(text); + new FakeSVG('text', { + "x": x + 15, + "y": y + 4, + "class": "diagram-text" + }, (this.type=="any"?"1+":"all")).addTo(text); + new FakeSVG('path', { + "d": "M "+(x+this.width-20)+" "+(y-10)+" h 16 a 4 4 0 0 1 4 4 v 12 a 4 4 0 0 1 -4 4 h -16 z", + "class": "diagram-text" + }).addTo(text); + new FakeSVG('path', { + "d": "M "+(x+this.width-13)+" "+(y-2)+" a 4 4 0 1 0 6 -1 m 2.75 -1 h -4 v 4 m 0 -3 h 2", + "style": "stroke-width: 1.75" + }).addTo(text); + return this; + } + toTextDiagram() { + var [multi_repeat] = TextDiagram._getParts(["multi_repeat"]); + if(this.type == "any") { + var anyAll = TextDiagram.rect("1+"); + } else { + anyAll = TextDiagram.rect("all"); + } + var diagramTD = Choice.prototype.toTextDiagram.call(this); + var repeatTD = TextDiagram.rect(multi_repeat); + diagramTD = anyAll.appendRight(diagramTD, ""); + diagramTD = diagramTD.appendRight(repeatTD, ""); + return diagramTD; + } +} +funcs.MultipleChoice = (...args)=>new MultipleChoice(...args); + + +export class Optional extends FakeSVG { + constructor(item, skip) { + if( skip === undefined ) + return new Choice(1, new Skip(), item); + else if ( skip === "skip" ) + return new Choice(0, new Skip(), item); + else + throw "Unknown value for Optional()'s 'skip' argument."; + } +} +funcs.Optional = (...args)=>new Optional(...args); + + +export class OneOrMore extends FakeSVG { + constructor(item, rep) { + super('g'); + rep = rep || (new Skip()); + this.item = wrapString(item); + this.rep = wrapString(rep); + this.width = Math.max(this.item.width, this.rep.width) + Options.AR*2; + this.height = this.item.height; + this.up = this.item.up; + this.down = Math.max(Options.AR*2, this.item.down + Options.VS + this.rep.up + this.rep.height + this.rep.down); + this.needsSpace = true; + if(Options.DEBUG) { + this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; + this.attrs['data-type'] = "oneormore"; + } + } + format(x,y,width) { + // Hook up the two sides if this is narrower than its stated width. + var gaps = determineGaps(width, this.width); + new Path(x,y).h(gaps[0]).addTo(this); + new Path(x+gaps[0]+this.width,y+this.height).h(gaps[1]).addTo(this); + x += gaps[0]; + + // Draw item + new Path(x,y).right(Options.AR).addTo(this); + this.item.format(x+Options.AR,y,this.width-Options.AR*2).addTo(this); + new Path(x+this.width-Options.AR,y+this.height).right(Options.AR).addTo(this); + + // Draw repeat arc + var distanceFromY = Math.max(Options.AR*2, this.item.height+this.item.down+Options.VS+this.rep.up); + new Path(x+Options.AR,y).arc('nw').down(distanceFromY-Options.AR*2).arc('ws').addTo(this); + this.rep.format(x+Options.AR, y+distanceFromY, this.width - Options.AR*2).addTo(this); + new Path(x+this.width-Options.AR, y+distanceFromY+this.rep.height).arc('se').up(distanceFromY-Options.AR*2+this.rep.height-this.item.height).arc('en').addTo(this); + + return this; + } + toTextDiagram() { + var [line, repeat_top_left, repeat_left, repeat_bot_left, repeat_top_right, repeat_right, repeat_bot_right] = TextDiagram._getParts(["line", "repeat_top_left", "repeat_left", "repeat_bot_left", "repeat_top_right", "repeat_right", "repeat_bot_right"]); + // Format the item and then format the repeat append it to tbe bottom, after a spacer. + var itemTD = this.item.toTextDiagram(); + var repeatTD = this.rep.toTextDiagram(); + var fIRWidth = TextDiagram._maxWidth(itemTD, repeatTD); + repeatTD = repeatTD.expand(0, fIRWidth - repeatTD.width, 0, 0); + itemTD = itemTD.expand(0, fIRWidth - itemTD.width, 0, 0); + var itemAndRepeatTD = itemTD.appendBelow(repeatTD, []); + // Build the left side of the repeat line and append the combined item and repeat to its right. + var leftLines = []; + leftLines.push(repeat_top_left + line); + for(var i = 0; i < ((itemTD.height - itemTD.entry) + repeatTD.entry - 1); i++) { + leftLines.push(repeat_left + " "); + } + leftLines.push(repeat_bot_left + line); + var leftTD = new TextDiagram(0, 0, leftLines); + leftTD = leftTD.appendRight(itemAndRepeatTD, ""); + // Build the right side of the repeat line and append it to the combined left side, item, and repeat's right. + var rightLines = []; + rightLines.push(line + repeat_top_right); + for(i = 0; i < ((itemTD.height - itemTD.exit) + repeatTD.exit - 1); i++) { + rightLines.push(" " + repeat_right); + } + rightLines.push(line + repeat_bot_right); + var rightTD = new TextDiagram(0, 0, rightLines); + var diagramTD = leftTD.appendRight(rightTD, ""); + return diagramTD; + } + walk(cb) { + cb(this); + this.item.walk(cb); + this.rep.walk(cb); + } +} +funcs.OneOrMore = (...args)=>new OneOrMore(...args); + + +export class ZeroOrMore extends FakeSVG { + constructor(item, rep, skip) { + return new Optional(new OneOrMore(item, rep), skip); + } +} +funcs.ZeroOrMore = (...args)=>new ZeroOrMore(...args); + + +export class Group extends FakeSVG { + constructor(item, label) { + super('g'); + this.item = wrapString(item); + this.label = + label instanceof FakeSVG + ? label + : label + ? new Comment(label) + : undefined; + + this.width = Math.max( + this.item.width + (this.item.needsSpace?20:0), + this.label ? this.label.width : 0, + Options.AR*2); + this.height = this.item.height; + this.boxUp = this.up = Math.max(this.item.up + Options.VS, Options.AR); + if(this.label) { + this.up += this.label.up + this.label.height + this.label.down; + } + this.down = Math.max(this.item.down + Options.VS, Options.AR); + this.needsSpace = true; + if(Options.DEBUG) { + this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; + this.attrs['data-type'] = "group"; + } + } + format(x, y, width) { + var gaps = determineGaps(width, this.width); + new Path(x,y).h(gaps[0]).addTo(this); + new Path(x+gaps[0]+this.width,y+this.height).h(gaps[1]).addTo(this); + x += gaps[0]; + + new FakeSVG('rect', { + x, + y:y-this.boxUp, + width:this.width, + height:this.boxUp + this.height + this.down, + rx: Options.AR, + ry: Options.AR, + 'class':'group-box', + }).addTo(this); + + this.item.format(x,y,this.width).addTo(this); + if(this.label) { + this.label.format( + x, + y-(this.boxUp+this.label.down+this.label.height), + this.label.width).addTo(this); + } + + return this; + } + toTextDiagram() { + var diagramTD = TextDiagram.roundrect(this.item.toTextDiagram(), true); + if(this.label != undefined) { + var labelTD = this.label.toTextDiagram(); + diagramTD = labelTD.appendBelow(diagramTD, [], true, true).expand(0, 0, 1, 0); + } + return diagramTD + } + walk(cb) { + cb(this); + this.item.walk(cb); + this.label.walk(cb); + } +} +funcs.Group = (...args)=>new Group(...args); + + +export class Start extends FakeSVG { + constructor({type="simple", label}={}) { + super('g'); + this.width = 20; + this.height = 0; + this.up = 10; + this.down = 10; + this.type = type; + if(label) { + this.label = ""+label; + this.width = Math.max(20, this.label.length * Options.CHAR_WIDTH + 10); + } + if(Options.DEBUG) { + this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; + this.attrs['data-type'] = "start"; + } + } + format(x,y) { + let path = new Path(x, y-10); + if (this.type === "complex") { + path.down(20) + .m(0, -10) + .right(this.width) + .addTo(this); + } else { + path.down(20) + .m(10, -20) + .down(20) + .m(-10, -10) + .right(this.width) + .addTo(this); + } + if(this.label) { + new FakeSVG('text', {x:x, y:y-15, style:"text-anchor:start"}, this.label).addTo(this); + } + return this; + } + toTextDiagram() { + var [cross, line, tee_right] = TextDiagram._getParts(["cross", "line", "tee_right"]) + if (this.type === "simple") { + var start = tee_right + cross + line; + } else { + start = tee_right + line; + } + var labelTD = new TextDiagram(0, 0, []); + if (this.label != undefined) { + labelTD = new TextDiagram(0, 0, [this.label]); + start = TextDiagram._padR(start, labelTD.width, line); + } + var startTD = new TextDiagram(0, 0, [start]); + return labelTD.appendBelow(startTD, [], true, true); + } +} +funcs.Start = (...args)=>new Start(...args); + + +export class End extends FakeSVG { + constructor({type="simple"}={}) { + super('path'); + this.width = 20; + this.height = 0; + this.up = 10; + this.down = 10; + this.type = type; + if(Options.DEBUG) { + this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; + this.attrs['data-type'] = "end"; + } + } + format(x,y) { + if (this.type === "complex") { + this.attrs.d = 'M '+x+' '+y+' h 20 m 0 -10 v 20'; + } else { + this.attrs.d = 'M '+x+' '+y+' h 20 m -10 -10 v 20 m 10 -20 v 20'; + } + return this; + } + toTextDiagram() { + var [cross, line, tee_left] = TextDiagram._getParts(["cross", "line", "tee_left"]); + if (this.type === "simple") { + var end = line + cross + tee_left; + } else { + end = line + tee_left; + } + return new TextDiagram(0, 0, [end]); + } +} +funcs.End = (...args)=>new End(...args); + + +export class Terminal extends FakeSVG { + constructor(text, {href, title, cls}={}) { + super('g', {'class': ['terminal', cls].join(" ")}); + this.text = ""+text; + this.href = href; + this.title = title; + this.cls = cls; + this.width = this.text.length * Options.CHAR_WIDTH + 20; /* Assume that each char is .5em, and that the em is 16px */ + this.height = 0; + this.up = 11; + this.down = 11; + this.textOffsetY = 12; + this.needsSpace = true; + if(Options.DEBUG) { + this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; + this.attrs['data-type'] = "terminal"; + } + } + format(x, y, width) { + // Hook up the two sides if this is narrower than its stated width. + var gaps = determineGaps(width, this.width); + new Path(x,y).h(gaps[0]).addTo(this); + new Path(x+gaps[0]+this.width,y).h(gaps[1]).addTo(this); + x += gaps[0]; + + new FakeSVG('rect', {x:x, y:y-this.textOffsetY, width:this.width, height:this.up+this.down, rx:10, ry:10}).addTo(this); + var text = new FakeSVG('text', {x:x+this.width/2, y:y+4}, this.text); + if(this.href) + new FakeSVG('a', {'xlink:href': this.href}, [text]).addTo(this); + else + text.addTo(this); + if(this.title) + new FakeSVG('title', {}, [this.title]).addTo(this); + return this; + } + toTextDiagram() { + // Note: href, title, and cls are ignored for text diagrams. + return TextDiagram.roundrect(this.text); + } +} +funcs.Terminal = (...args)=>new Terminal(...args); + + +export class NonTerminal extends FakeSVG { + constructor(text, {href, title, cls="", attrs}={}) { + super('g', {'class': ['non-terminal', cls].join(" ")}); + this.text = ""+text; + this.href = href; + this.title = title; + this.cls = cls; + this.width = this.text.length * Options.CHAR_WIDTH + 20; + this.height = 0; + this.up = 11; + this.down = 11; + this.textOffsetY = 12; + this.needsSpace = true; + if (typeof attrs === "object" && attrs !== null) { + Object.assign(this.attrs, attrs); + } + if(Options.DEBUG) { + this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; + this.attrs['data-type'] = "nonterminal"; + } + } + format(x, y, width) { + // Hook up the two sides if this is narrower than its stated width. + var gaps = determineGaps(width, this.width); + new Path(x,y).h(gaps[0]).addTo(this); + new Path(x+gaps[0]+this.width,y).h(gaps[1]).addTo(this); + x += gaps[0]; + + new FakeSVG('rect', {x:x, y:y-this.textOffsetY, width:this.width, height:this.up+this.down, rx: 2, ry: 2}).addTo(this); + var text = new FakeSVG('text', {x:x+this.width/2, y:y+4}, this.text); + if(this.href) + new FakeSVG('a', {'xlink:href': this.href}, [text]).addTo(this); + else + text.addTo(this); + if(this.title) + new FakeSVG('title', {}, [this.title]).addTo(this); + return this; + } + toTextDiagram() { + // Note: href, title, and cls are ignored for text diagrams. + return TextDiagram.rect(this.text); + } +} +funcs.NonTerminal = (...args)=>new NonTerminal(...args); + + +export class Comment extends FakeSVG { + constructor(text, {href, title, cls=""}={}) { + super('g', {'class': ['comment', cls].join(" ")}); + this.text = ""+text; + this.href = href; + this.title = title; + this.cls = cls; + this.width = this.text.length * Options.COMMENT_CHAR_WIDTH + 10; + this.height = 0; + this.up = 8; + this.down = 8; + this.needsSpace = true; + if(Options.DEBUG) { + this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; + this.attrs['data-type'] = "comment"; + } + } + format(x, y, width) { + // Hook up the two sides if this is narrower than its stated width. + var gaps = determineGaps(width, this.width); + new Path(x,y).h(gaps[0]).addTo(this); + new Path(x+gaps[0]+this.width,y+this.height).h(gaps[1]).addTo(this); + x += gaps[0]; + + var text = new FakeSVG('text', {x:x+this.width/2, y:y+5, class:'comment'}, this.text); + if(this.href) + new FakeSVG('a', {'xlink:href': this.href}, [text]).addTo(this); + else + text.addTo(this); + if(this.title) + new FakeSVG('title', {}, this.title).addTo(this); + return this; + } + toTextDiagram() { + // Note: href, title, and cls are ignored for text diagrams. + return new TextDiagram(0, 0, [this.text]); + } +} +funcs.Comment = (...args)=>new Comment(...args); + + +export class Skip extends FakeSVG { + constructor() { + super('g'); + this.width = 0; + this.height = 0; + this.up = 0; + this.down = 0; + this.needsSpace = false; + if(Options.DEBUG) { + this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; + this.attrs['data-type'] = "skip"; + } + } + format(x, y, width) { + new Path(x,y).right(width).addTo(this); + return this; + } + toTextDiagram() { + var [line] = TextDiagram._getParts(["line"]); + return new TextDiagram(0, 0, [line]); + } +} +funcs.Skip = (...args)=>new Skip(...args); + + +export class Block extends FakeSVG { + constructor({width=50, up=15, height=25, down=15, needsSpace=true}={}) { + super('g'); + this.width = width; + this.height = height; + this.up = up; + this.down = down; + this.needsSpace = true; + if(Options.DEBUG) { + this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; + this.attrs['data-type'] = "block"; + } + } + format(x, y, width) { + // Hook up the two sides if this is narrower than its stated width. + var gaps = determineGaps(width, this.width); + new Path(x,y).h(gaps[0]).addTo(this); + new Path(x+gaps[0]+this.width,y).h(gaps[1]).addTo(this); + x += gaps[0]; + + new FakeSVG('rect', {x:x, y:y-this.up, width:this.width, height:this.up+this.height+this.down}).addTo(this); + return this; + } + toTextDiagram() { + return TextDiagram.rect(""); + } +} +funcs.Block = (...args)=>new Block(...args); + +export class TextDiagram { + constructor(entry, exit, lines) { + // entry: The entry line for this diagram-part. + this.entry = entry; + // exit: The exit line for this diagram-part. + this.exit = exit; + // height: The height of this diagram-part, in lines. + this.height = lines.length; + // lines[]: The visual data of this diagram-part. Each line must be the same length. + this.lines = Array.from(lines); + // width: The width of this diagram-part, in character cells. + this.width = lines.length > 0 ? lines[0].length : 0; + if(entry > lines.length) { + throw new Error("Entry is not within diagram vertically:\n" + this._dump(false)); + } + if(exit > lines.length) { + throw new Error("Exit is not within diagram vertically:\n" + this._dump(false)); + } + for(var i=0; i < lines.length; i++) { + if(lines[0].length != lines[i].length) { + throw new Error("Diagram data is not rectangular:\n" + this._dump(false)); + } + } + } + alter(entry=null, exit=null, lines=null) { + /* + Create and return a new TextDiagram based on this instance, with the specified changes. + + Note: This is used sparingly, and may be a bad idea. + */ + var newEntry = entry || this.entry; + var newExit = exit || this.exit; + var newLines = lines || this.lines; + return new TextDiagram(newEntry, newExit, Array.from(newLines)); + } + appendBelow(item, linesBetween, moveEntry=false, moveExit=false) { + /* + Create and return a new TextDiagram by appending the specified lines below this instance's data, + and then appending the specified TextDiagram below those lines, possibly setting the resulting + TextDiagram's entry and or exit indices to those of the appended item. + */ + var newWidth = Math.max(this.width, item.width); + var newLines = []; + var centeredLines = this.center(newWidth, " ").lines + for(const line of centeredLines) { + newLines.push(line); + } + for(const line of linesBetween) { + newLines.push(TextDiagram._padR(line, newWidth, " ")); + } + centeredLines = item.center(newWidth, " ").lines + for(const line of centeredLines) { + newLines.push(line); + } + var newEntry = moveEntry ? this.height + linesBetween.length + item.entry : this.entry; + var newExit = moveExit ? this.height + linesBetween.length + item.exit : this.exit; + return new TextDiagram(newEntry, newExit, newLines); + } + appendRight(item, charsBetween) { + /* + Create and return a new TextDiagram by appending the specified TextDiagram to the right of this instance's data, + aligning the left-hand exit and the right-hand entry points. The charsBetween are inserted between the left-exit + and right-entry, and equivalent spaces on all other lines. + */ + var joinLine = Math.max(this.exit, item.entry); + var newHeight = Math.max(this.height - this.exit, item.height - item.entry) + joinLine; + var leftTopAdd = joinLine - this.exit; + var leftBotAdd = newHeight - this.height - leftTopAdd; + var rightTopAdd = joinLine - item.entry; + var rightBotAdd = newHeight - item.height - rightTopAdd; + var left = this.expand(0, 0, leftTopAdd, leftBotAdd); + var right = item.expand(0, 0, rightTopAdd, rightBotAdd); + var newLines = []; + for(var i = 0; i < newHeight; i++) { + var sep = i != joinLine ? " ".repeat(charsBetween.length) : charsBetween; + newLines.push((left.lines[i] + sep + right.lines[i])); + } + var newEntry = this.entry + leftTopAdd; + var newExit = item.exit + rightTopAdd; + return new TextDiagram(newEntry, newExit, newLines); + } + center(width, pad) { + /* + Create and return a new TextDiagram by centering the data of this instance within a new, equal or larger widtth. + */ + if(width < this.width) { + throw new Error("Cannot center into smaller width") + } + if(width === this.width) { + return this.copy(); + } else { + var totalPadding = width - this.width; + var leftWidth = Math.trunc(totalPadding / 2); + var left = []; + for (var i = 0; i < this.height; i++) { + left.push(pad.repeat(leftWidth)); + } + var right = []; + for (i = 0; i < this.height; i++) { + right.push(pad.repeat(totalPadding - leftWidth)); + } + return new TextDiagram(this.entry, this.exit, TextDiagram._encloseLines(this.lines, left, right)); + } + } + copy() { + /* + Create and return a new TextDiagram by copying this instance's data. + */ + return new TextDiagram(this.entry, this.exit, Array.from(this.lines)); + } + expand(left, right, top, bottom) { + /* + Create and return a new TextDiagram by expanding this instance's data by the specified amount in the specified directions. + */ + if(left < 0 || right < 0 || top < 0 || bottom < 0) { + throw new Error("Expansion values cannot be negative"); + } + if(left + right + top + bottom === 0) { + return this.copy(); + } else { + var line = TextDiagram.parts["line"]; + var newLines = []; + for(var i = 0; i < top; i++) { + newLines.push(" ".repeat(this.width + left + right)); + } + for(i = 0; i < this.height; i++){ + var leftExpansion = i === this.entry ? line : " "; + var rightExpansion = i === this.exit ? line : " "; + newLines.push(leftExpansion.repeat(left) + this.lines[i] + rightExpansion.repeat(right)); + } + for(i = 0; i < bottom; i++) { + newLines.push(" ".repeat(this.width + left + right)); + } + return new TextDiagram(this.entry + top, this.exit + top, newLines); + } + } + static rect(item, dashed=false) { + /* + Create and return a new TextDiagram for a rectangular box. + */ + return TextDiagram._rectish("rect", item, dashed); + } + static roundrect(item, dashed=false) { + /* + Create and return a new TextDiagram for a rectangular box with rounded corners. + */ + return TextDiagram._rectish("roundrect", item, dashed); + } + static setFormatting(characters = null, defaults = null) { + /* + Set the characters to use for drawing text diagrams. + */ + if(characters !== null) { + TextDiagram.parts = {} + if(defaults !== null){ + TextDiagram.parts = {...TextDiagram.parts, ...defaults} + } + TextDiagram.parts = {...TextDiagram.parts, ...characters} + } + for(const [name, value] of TextDiagram.parts) { + if(value.length != 1) { + "Text part " + name + " is more than 1 character: " + value; + } + } + } + _dump(show=true) { + /* + Dump out the data of this instance for debugging, either displaying or returning it. + DO NOT use this for actual work, only for debugging or in assertion output. + */ + var nl = "\n" // f-strings can't contain \n until Python 3.12; + var result = "height=" + this.height + " lines.length=" + this.lines.length; + if(this.entry > this.lines.length) { + result += "; entry outside diagram: entry=" + this.entry; + } + if(this.exit > this.lines.length) { + result += "; exit outside diagram: exit=" + this.exit; + } + for(var y = 0; y < Math.max(this.lines.length, this.entry + 1, this.exit + 1); y++) { + result = result + nl + "[" + ("00" + y).slice(-3) + "]"; + if(y < this.lines.length) { + result += (" '" + this.lines[y] + "' len=" + this.lines[y].length); + } + if(y === this.entry && y === this.exit) { + result += " <- entry, exit"; + } else if(y === this.entry) { + result += " <- entry"; + } else if(y === this.exit) { + result += " <- exit"; + } + } + if(show) { + console.log(result); + } + return result; + } + static _encloseLines(lines, lefts, rights) { + /* + Join the lefts, lines, and rights arrays together, line-by-line, and return the result. + */ + if(lines.length != lefts.length) { + throw new Error("All arguments must be the same length"); + } + if(lines.length != rights.length) { + throw new Error("All arguments must be the same length"); + } + var newLines = []; + for(var i = 0; i < lines.length; i++) { + newLines.push(lefts[i] + lines[i] + rights[i]); + } + return newLines; + } + static _gaps(outerWidth, innerWidth) { + /* + Return the left and right pad spacing based on the alignment configuration setting. + */ + var diff = outerWidth - innerWidth; + if(Options.INTERNAL_ALIGNMENT === "left") { + return [0, diff]; + } else if(Options.INTERNAL_ALIGNMENT === "right") { + return [diff, 0]; + } else { + var left = Math.trunc(diff / 2); + var right = diff - left; + return [left, right]; + } + } + static _getParts(partNames) { + /* + Return a list of text diagram drawing characters for the specified character names. + */ + var result = []; + for(const name of partNames) { + if(TextDiagram.parts[name] == undefined) { + throw new Error("Text diagram part " + name + "not found.") + } + result.push(TextDiagram.parts[name]); + } + return result; + } + static _maxWidth(...args) { + /* + Return the maximum width of all of the arguments. + */ + var maxWidth = 0; + for(const arg of args) { + if(arg instanceof TextDiagram) { + var width = arg.width; + } else if(arg instanceof Array) { + width = Math.max(arg.map(function(e) { return e.length;})); + } else if(Number.isInteger(arg)) { + width = Number.toString(arg).length; + } else { + width = arg.length; + } + maxWidth = width > maxWidth ? width : maxWidth; + } + return maxWidth; + } + static _padL(string, width, pad) { + /* + Pad the specified string on the left to the specified width with the specified pad string and return the result. + */ + if((width - string.length) % pad.length != 0) { + throw new Error("Gap " + (width - string.length) + " must be a multiple of pad string '" + pad + "'"); + } + return pad.repeat(Math.trunc((width - string.length / pad.length))) + string; + } + static _padR(string, width, pad) { + /* + Pad the specified string on the right to the specified width with the specified pad string and return the result. + */ + if((width - string.length) % pad.length != 0) { + throw new Error("Gap " + (width - string.length) + " must be a multiple of pad string '" + pad + "'"); + } + return string + pad.repeat(Math.trunc((width - string.length / pad.length))); + } + static _rectish(rectType, data, dashed=false) { + /* + Create and return a new TextDiagram for a rectangular box surrounding the specified TextDiagram, using the + specified set of drawing characters (i.e., "rect" or "roundrect"), and possibly using dashed lines. + */ + var lineType = dashed ? "_dashed" : ""; + var [topLeft, ctrLeft, botLeft, topRight, ctrRight, botRight, topHoriz, botHoriz, line, cross] = TextDiagram._getParts([rectType + "_top_left", rectType + "_left" + lineType, rectType + "_bot_left", rectType + "_top_right", rectType + "_right" + lineType, rectType + "_bot_right", rectType + "_top" + lineType, rectType + "_bot" + lineType, "line", "cross"]); + var itemWasFormatted = data instanceof TextDiagram; + if(itemWasFormatted) { + var itemTD = data; + } else { + itemTD = new TextDiagram(0, 0, [data]); + } + // Create the rectangle and enclose the item in it. + var lines = []; + lines.push(topHoriz.repeat(itemTD.width + 2)); + if(itemWasFormatted) { + lines += itemTD.expand(1, 1, 0, 0).lines; + } else { + for(var i = 0; i < itemTD.lines.length; i++) { + lines.push(" " + itemTD.lines[i] + " "); + } + } + lines.push(botHoriz.repeat(itemTD.width + 2)); + var entry = itemTD.entry + 1; + var exit = itemTD.exit + 1; + var leftMaxWidth = TextDiagram._maxWidth(topLeft, ctrLeft, botLeft); + var lefts = []; + lefts.push(TextDiagram._padR(topLeft, leftMaxWidth, topHoriz)); + for(i = 1; i < (lines.length - 1); i++) { + lefts.push(TextDiagram._padR(ctrLeft, leftMaxWidth, " ")); + } + lefts.push(TextDiagram._padR(botLeft, leftMaxWidth, botHoriz)); + if(itemWasFormatted) { + lefts[entry] = cross; + } + var rightMaxWidth = TextDiagram._maxWidth(topRight, ctrRight, botRight); + var rights = []; + rights.push(TextDiagram._padL(topRight, rightMaxWidth, topHoriz)); + for(i = 1; i < (lines.length - 1); i++) { + rights.push(TextDiagram._padL(ctrRight, rightMaxWidth, " ")); + } + rights.push(TextDiagram._padL(botRight, rightMaxWidth, botHoriz)); + if(itemWasFormatted) { + rights[exit] = cross; + } + // Build the entry and exit perimeter. + lines = TextDiagram._encloseLines(lines, lefts, rights); + lefts = []; + for(i = 0; i < lines.length; i++) { + lefts.push(" "); + } + lefts[entry] = line; + rights = []; + for(i = 0; i < lines.length; i++) { + rights.push(" "); + } + rights[exit] = line; + lines = TextDiagram._encloseLines(lines, lefts, rights); + return new TextDiagram(entry, exit, lines); + } + // Note: All the drawing sequences below MUST be single characters. setFormatting() checks this. + // Unicode 25xx box drawing characters, plus a few others. + static PARTS_UNICODE = { + "cross_diag" : "\u2573", + "corner_bot_left" : "\u2514", + "corner_bot_right" : "\u2518", + "corner_top_left" : "\u250c", + "corner_top_right" : "\u2510", + "cross" : "\u253c", + "left" : "\u2502", + "line" : "\u2500", + "line_vertical" : "\u2502", + "multi_repeat" : "\u21ba", + "rect_bot" : "\u2500", + "rect_bot_dashed" : "\u2504", + "rect_bot_left" : "\u2514", + "rect_bot_right" : "\u2518", + "rect_left" : "\u2502", + "rect_left_dashed" : "\u2506", + "rect_right" : "\u2502", + "rect_right_dashed" : "\u2506", + "rect_top" : "\u2500", + "rect_top_dashed" : "\u2504", + "rect_top_left" : "\u250c", + "rect_top_right" : "\u2510", + "repeat_bot_left" : "\u2570", + "repeat_bot_right" : "\u256f", + "repeat_left" : "\u2502", + "repeat_right" : "\u2502", + "repeat_top_left" : "\u256d", + "repeat_top_right" : "\u256e", + "right" : "\u2502", + "roundcorner_bot_left" : "\u2570", + "roundcorner_bot_right" : "\u256f", + "roundcorner_top_left" : "\u256d", + "roundcorner_top_right" : "\u256e", + "roundrect_bot" : "\u2500", + "roundrect_bot_dashed" : "\u2504", + "roundrect_bot_left" : "\u2570", + "roundrect_bot_right" : "\u256f", + "roundrect_left" : "\u2502", + "roundrect_left_dashed" : "\u2506", + "roundrect_right" : "\u2502", + "roundrect_right_dashed" : "\u2506", + "roundrect_top" : "\u2500", + "roundrect_top_dashed" : "\u2504", + "roundrect_top_left" : "\u256d", + "roundrect_top_right" : "\u256e", + "separator" : "\u2500", + "tee_left" : "\u2524", + "tee_right" : "\u251c", + }; + // Plain old ASCII characters. + static PARTS_ASCII = { + "cross_diag" : "X", + "corner_bot_left" : "\\", + "corner_bot_right" : "/", + "corner_top_left" : "/", + "corner_top_right" : "\\", + "cross" : "+", + "left" : "|", + "line" : "-", + "line_vertical" : "|", + "multi_repeat" : "&", + "rect_bot" : "-", + "rect_bot_dashed" : "-", + "rect_bot_left" : "+", + "rect_bot_right" : "+", + "rect_left" : "|", + "rect_left_dashed" : "|", + "rect_right" : "|", + "rect_right_dashed" : "|", + "rect_top_dashed" : "-", + "rect_top" : "-", + "rect_top_left" : "+", + "rect_top_right" : "+", + "repeat_bot_left" : "\\", + "repeat_bot_right" : "/", + "repeat_left" : "|", + "repeat_right" : "|", + "repeat_top_left" : "/", + "repeat_top_right" : "\\", + "right" : "|", + "roundcorner_bot_left" : "\\", + "roundcorner_bot_right" : "/", + "roundcorner_top_left" : "/", + "roundcorner_top_right" : "\\", + "roundrect_bot" : "-", + "roundrect_bot_dashed" : "-", + "roundrect_bot_left" : "\\", + "roundrect_bot_right" : "/", + "roundrect_left" : "|", + "roundrect_left_dashed" : "|", + "roundrect_right" : "|", + "roundrect_right_dashed" : "|", + "roundrect_top" : "-", + "roundrect_top_dashed" : "-", + "roundrect_top_left" : "/", + "roundrect_top_right" : "\\", + "separator" : "-", + "tee_left" : "|", + "tee_right" : "|", + }; + + // Characters to use in drawing diagrams. See setFormatting(), PARTS_ASCII, and PARTS_UNICODE. + static parts = TextDiagram.PARTS_UNICODE; + +} + +function unnull(...args) { + // Return the first value that isn't undefined. + // More correct than `v1 || v2 || v3` because falsey values will be returned. + return args.reduce(function(sofar, x) { return sofar !== undefined ? sofar : x; }); +} + +function determineGaps(outer, inner) { + var diff = outer - inner; + switch(Options.INTERNAL_ALIGNMENT) { + case 'left': return [0, diff]; + case 'right': return [diff, 0]; + default: return [diff/2, diff/2]; + } +} + +function wrapString(value) { + return value instanceof FakeSVG ? value : new Terminal(""+value); +} + +function sum(iter, func) { + if(!func) func = function(x) { return x; }; + return iter.map(func).reduce(function(a,b){return a+b}, 0); +} + +function max(iter, func=x=>x) { + return Math.max.apply(null, iter.map(func)); +} + +function SVG(name, attrs, text) { + attrs = attrs || {}; + text = text || ''; + var el = document.createElementNS("http://www.w3.org/2000/svg",name); + for(var attr in attrs) { + if(attr === 'xlink:href') + el.setAttributeNS("http://www.w3.org/1999/xlink", 'href', attrs[attr]); + else + el.setAttribute(attr, attrs[attr]); + } + el.textContent = text; + return el; +} + +function escapeString(string) { + // Escape markdown and HTML special characters + return string.replace(/[*_\`\[\]<&]/g, function(charString) { + return '&#' + charString.charCodeAt(0) + ';'; + }); +} + +function* enumerate(iter) { + var count = 0; + for(const x of iter) { + yield [count, x]; + count++; + } +} diff --git a/hkmc2/shared/src/test/mlscript-packages/parsing-web-demo/Examples.mls b/hkmc2/shared/src/test/mlscript-packages/parsing-web-demo/Examples.mls new file mode 100644 index 0000000000..03d2ac8396 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/parsing-web-demo/Examples.mls @@ -0,0 +1,55 @@ +import "std/MutMap.mls" +import "std/Predef.mls" + +open Predef +module Examples with... + +val examples = MutMap.empty + +examples |> MutMap.insert of + "hanoi" + name: "Hanoi from Caml Light" + source: """let spaces n = make_string n " ";; +let disk size = + let right_half = make_string size ">" + and left_half = make_string size "<" + in left_half ^ "|" ^ right_half;; +let disk_number n largest_disk_size = + let white_part = spaces (largest_disk_size + 1 - n) in + white_part ^ (disk n) ^ white_part;; +let peg_base largest_disk_size = + let half = make_string largest_disk_size "_" in + " " ^ half ^ "|" ^ half ^ " ";; +let rec peg largest_disk_size = function + | (0, []) -> [] + | (0, head::rest) -> + disk_number head largest_disk_size :: + peg largest_disk_size (0, rest) + | (offset, lst) -> + disk_number 0 largest_disk_size :: + peg largest_disk_size (offset-1, lst);; +let rec join_lines l1 l2 l3 = + match (l1, l2, l3) with + | ([], [], []) -> [] + | (t1::r1, t2::r2, t3::r3) -> (t1 ^ t2 ^ t3) :: join_lines r1 r2 r3 + | _ -> failwith "join_lines";; +""" + +examples |> MutMap.insert of + "extensible" + name: "Extensible Syntax" + source: """#newKeyword ("hello", Some 3, Some 3) +#newKeyword ("goodbye", None, None) + +#newCategory("greeting") + +#extendCategory("greeting", [ keyword("hello"), "term", "greeting" ], foo) +#extendCategory("greeting", [ keyword("goodbye") ], bar) + +#extendCategory("decl", [ "greeting" ], baz) + + +hello "Rob" hello "Bob" goodbye + +#diagram "" +""" diff --git a/hkmc2/shared/src/test/mlscript-packages/parsing-web-demo/index.html b/hkmc2/shared/src/test/mlscript-packages/parsing-web-demo/index.html new file mode 100644 index 0000000000..2dd8e2a147 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/parsing-web-demo/index.html @@ -0,0 +1,259 @@ + + + + + + + Document + + + + + +
+
+
+ + +
+ +
+
+
+
+

Syntax Diagrams

+
+
+ + + + + diff --git a/hkmc2/shared/src/test/mlscript-packages/parsing-web-demo/main.mls b/hkmc2/shared/src/test/mlscript-packages/parsing-web-demo/main.mls new file mode 100644 index 0000000000..a41f59e8e4 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/parsing-web-demo/main.mls @@ -0,0 +1,203 @@ +// Note: This file does not have a corresponding test file. It is recommended to +// test it through the following steps. +// +// 1. Install `serve` globally using `npm install -g serve`. +// 2. Run `serve hkmc2/shared/src/test/mlscript-compile` in the project's root. +// 3. Open the URL `http://localhost:3000/apps/parsing-web-demo` in your browser. +// If port 3000 is not available, use the URL suggested by the serve command. +// 4. Try and test it in the browser. +// +// Note that after the file is updated, we need to manually refresh the browser. +// Or we can use Live Server extension (or its alternatives) in VSCode to +// automatically refresh the browser. + +import "gpp/Parser.mls" +import "gpp/Lexer.mls" +import "std/StrOps.mls" +import "std/Iter.mls" +import "std/XML.mls" +import "std/Option.mls" +import "std/Runtime.mls" +import "std/Predef.mls" +import "gpp/TreeHelpers.mls" +import "gpp/Extension.mls" +import "gpp/ParseRuleVisualizer.mls" +import "gpp/Rules.mls" +import "gpp/thirdparty/railroad/railroad.mjs" +import "./Examples.mls" + +open XML { elem } +open Predef +open Option { Some, None } +open Examples + +module Main with ... + +let query = document.querySelector.bind(document) +let editor = query("#editor") +let selector = query("select#example") +let parseButton = query("button#parse") +let outputPanel = query("#output") + +examples Iter.each of case [key, example] then + let option = document.createElement of "option" + set option.value = key + set option.textContent = example.name + selector.appendChild of option + // Load the first example. + if editor.value is "" do + set editor.value = example.source + +// Make pressing Tab add two spaces at the caret's position. +editor.addEventListener of "keydown", event => + if event.key is "Tab" do + // Prevent the default tab behavior (focusing the next element). + event.preventDefault() + let start = editor.selectionStart + let end = editor.selectionEnd + set editor.value = editor.value.substring(0, start) + " " + editor.value.substring(end) + set editor.selectionEnd = start + 2 + set editor.selectionStart = editor.selectionEnd + +selector.addEventListener of "change", event => + if examples.get(selector.value) is + Some(example) do set editor.value = example.source + None do throw new Error of "Example \"" + selector.value + "\" not found" + +parseButton.addEventListener of "click", event => + let tokens = Lexer.lex(editor.value, noWhitespace: true) + set outputPanel.innerHTML = "" + Runtime.try_catch of + () => + let trees = Parser.parse(tokens) + trees Iter.fromStack() Iter.each of tree => if + Extension.isDiagramDirective(tree) then displayRules() + else + let collapsibleTree = document.createElement("collapsible-tree") + set collapsibleTree.textContent = TreeHelpers.showAsTree(tree) + outputPanel.appendChild of collapsibleTree + error => + let errorDisplay = document.createElement("error-display") + errorDisplay.setError of error + outputPanel.appendChild of errorDisplay + +let indentRegex = new RegExp("""^(\s*)""") + +fun parseIndentedText(text) = + let root = (text: "", children: []) + let stack = [(node: root, indent: -1)] + text.split("\n") + Iter.filtering(line => line.trim().length > 0) + Iter.each of line => + let indent = line.match(indentRegex).[1].length + let text = line.substring(indent) + while indent <= stack.[stack.length - 1].indent do + stack.pop() + let newNode = (text: text, children: []) + stack.[stack.length - 1].node.children.push of newNode + stack.push of node: newNode, indent: indent + root.children + +// * CSS styles for the error display. +// * The content in custom elements is placed inside the ShadowDOM, which is +// * scoped. Therefore, styles placed directly in HTML won't be effective on +// * elements inside custom elements. Placing CSS here is more convenient. +let errorDisplayStyle = """ +.error-container { + background-color: #fdd; + padding: 0.375rem 0.75rem 0.5rem; + font-family: var(--monospace); + color: #991b1bff; + display: flex; + flex-direction: column; + gap: 0.25rem; +} +.error-message { + margin: 0; + font-weight: bold; + font-size: 1.125rem; +} +.stack-trace { + font-size: 0.875rem; + margin: 0; + list-style-type: none; + padding-left: 0.5rem; +}""" + +class CollapsibleTree extends HTMLElement with + fun connectedCallback() = + let rawText = this.textContent + set this.textContent = "" + let treeData = parseIndentedText(rawText) + let treeElement = createDetailsTree(treeData) + this.appendChild(treeElement) + + fun createDetailsTree(nodes) = + let fragment = document.createDocumentFragment() + nodes Iter.each of node => + let details = document.createElement("details") + details.setAttribute("open", "") + let summary = document.createElement("summary") + set summary.textContent = node.text + details.appendChild of summary + if node.children.length > 0 then + details.appendChild of createDetailsTree(node.children) + else + details.setAttribute("leaf" ,"") + fragment.appendChild of details + let rule = document.createElement("rule") + rule.classList.add("rule") + fragment.appendChild of rule + fragment + +customElements.define of "collapsible-tree", CollapsibleTree + +class ErrorDisplay extends HTMLElement with + this.attachShadow(mode: "open") + + let _error = None + + fun connectedCallback() = this.render() + + fun setError(value) = + set _error = Some(value) + this.render() + + fun render() = if _error is Some(error) do + let stackLines = error.stack.split("\n") + if stackLines.[0].startsWith(error.name) do + stackLines.shift() + set this.shadowRoot.innerHTML = elem("div", "class": "error-container") of + elem("h3", "class": "error-message") of + error.name + ": " + error.message + elem("ul", "class": "stack-trace") of + stackLines + Iter.mapping of line => elem("li") of line.trim() + Iter.joined("") + elem("style") of errorDisplayStyle + +customElements.define of "error-display", ErrorDisplay + +fun makeFigures(entries) = entries + Iter.mapping of case [name, svg] then + elem("figure") of elem("figcaption")(name), svg + Iter.joined("") + +fun displayRules() = + open ParseRuleVisualizer + open Rules + reset() + set query("#syntax-diagrams>main").innerHTML = StrOps.concat of + elem("h3") of "Term" + makeFigures of render(railroad, "term", termRule) + elem("h3") of "Type" + makeFigures of render(railroad, "term", typeRule) + elem("h3") of "Definition" + makeFigures of render(railroad, "term", declRule) + elem("h3") of "Extension" + extendedKinds + Iter.mapping of kindName => + makeFigures of render(railroad, kindName, getRuleByKind(kindName)) + Iter.joined("") + +displayRules() diff --git a/hkmc2/shared/src/test/mlscript-packages/parsing-web-demo/manifest.json b/hkmc2/shared/src/test/mlscript-packages/parsing-web-demo/manifest.json new file mode 100644 index 0000000000..747371e86f --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/parsing-web-demo/manifest.json @@ -0,0 +1,32 @@ +{ + "name": "parsing-web-demo", + "main": "main.mls", + "moduleName": "Main", + "vendors": [ + { + "prefix": "std/", + "path": "../../mlscript-compile", + "files": [ + "Predef.mls", + "MutMap.mls", + "StrOps.mls", + "Iter.mls", + "XML.mls", + "Option.mls" + ] + }, + { + "prefix": "gpp/", + "path": "../generalized-pratt-parsing", + "files": [ + "Parser.mls", + "Lexer.mls", + "TreeHelpers.mls", + "Extension.mls", + "ParseRuleVisualizer.mls", + "Rules.mls" + ] + }, + {"prefix": "rd/", "path": "../recursive-descent-parsing", "files": ["RecursiveDescent.mls"]} + ] +} diff --git a/hkmc2/shared/src/test/mlscript-packages/recursive-descent-parsing/BasicExpr.mls b/hkmc2/shared/src/test/mlscript-packages/recursive-descent-parsing/BasicExpr.mls new file mode 100644 index 0000000000..8d1ed1c2a1 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/recursive-descent-parsing/BasicExpr.mls @@ -0,0 +1,34 @@ +import "std/StrOps.mls" +import "std/Option.mls" +import "std/Predef.mls" + +open StrOps { parenthesizedIf } +open Option { Some, None } +open Predef { mkStr } + +type OptionT[A] = Some[A] | None + +module BasicExpr with + type Expr = Lit | Var | Add | Mul | Err + + data + class + Lit(value: Int) + Var(name: Str) + Add(left: Expr, right: Expr) + Mul(left: Expr, right: Expr) + Err(expr: OptionT[Expr], msg: Str) + + fun withErr(expr, msg) = Err(Some(expr), msg) + fun justErr(msg) = Err(None, msg) + + fun prettyPrint(tree: Expr): Str = if tree is + Lit(value) then value.toString() + Var(name) then name + Add(left, right) then left prettyPrint() + " + " + right prettyPrint() + Mul(left, right) then StrOps.concat of + left prettyPrint() parenthesizedIf(left is Add) + " * " + right prettyPrint() parenthesizedIf(right is Add) + Err(Some(expr), msg) then "{ " + expr prettyPrint() + " | " + JSON.stringify(msg) + " }" + Err(None, msg) then "{ " + JSON.stringify(msg) + " }" diff --git a/hkmc2/shared/src/test/mlscript-packages/recursive-descent-parsing/RecursiveDescent.mls b/hkmc2/shared/src/test/mlscript-packages/recursive-descent-parsing/RecursiveDescent.mls new file mode 100644 index 0000000000..f9ce30ab39 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/recursive-descent-parsing/RecursiveDescent.mls @@ -0,0 +1,89 @@ +import "std/Predef.mls" +import "std/Stack.mls" +import "std/Option.mls" +import "std/Iter.mls" +import "gpp/Token.mls" +import "./BasicExpr.mls" + +open Predef { mkStr } +open Stack +open Option { Some, None } +open Token { Round, LiteralKind } +open BasicExpr { Expr } + +type StackT[A] = Cons[A] | Nil + +module RecursiveDescent with ... + +fun parse(tokens) = + fun advance = if tokens is + Token.Space :: tail then + set tokens = tail + advance + head :: tail then + set tokens = tail + Some(head) + Nil then None + + let peek = advance + + fun consume = set peek = advance + + fun require(result: Expr, expected) = if peek is + Some(actual) and + expected Token.same(actual) then + consume + result + else result BasicExpr.withErr of mkStr of + "Expected token " + expected Token.summary() + ", but found " + actual Token.summary() + None then result BasicExpr.withErr of mkStr of + "Expected token " + expected Token.summary() + ", but found end of input" + + fun atom: Expr = if peek is + Some of + Token.Literal(LiteralKind.Integer, literal) then + consume + BasicExpr.Lit(parseInt(literal, 10)) + Token.Identifier("(", true) then + consume + expr require of Token.Identifier(")", true) + Token.Identifier(name, false) then + consume + BasicExpr.Var(name) + token then BasicExpr.justErr of "Unexpected token " + token Token.summary() + None then BasicExpr.justErr of "Unexpected end of input" + + fun expr: Expr = + let leftmost = product + addSeq + Iter.fromStack() + Iter.folded of leftmost, BasicExpr.Add + + fun addSeq: StackT[Expr] = if peek is + Some(Token.Identifier("+", _)) then + consume + product :: addSeq + else Nil + + fun product: Expr = + let leftmost = atom + mulSeq + Iter.fromStack() + Iter.folded of leftmost, BasicExpr.Mul + + fun mulSeq: StackT[Expr] = if peek is + Some(Token.Identifier("*", _)) then + consume + atom :: mulSeq + else Nil + + let result = expr + if peek is + Some(token) then result BasicExpr.withErr of + "Expect end of input, but found " + token Token.summary() + None then result diff --git a/hkmc2/shared/src/test/mlscript-packages/recursive-descent-parsing/manifest.json b/hkmc2/shared/src/test/mlscript-packages/recursive-descent-parsing/manifest.json new file mode 100644 index 0000000000..a00e56db00 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/recursive-descent-parsing/manifest.json @@ -0,0 +1,19 @@ +{ + "name": "recursive-descent-parsing", + "main": "RecursiveDescent.mls", + "moduleName": "RecursiveDescent", + "vendors": [ + { + "prefix": "std/", + "path": "../../mlscript-compile", + "files": [ + "Predef.mls", + "Stack.mls", + "Option.mls", + "Iter.mls", + "StrOps.mls" + ] + }, + {"prefix": "gpp/", "path": "../generalized-pratt-parsing", "files": ["Token.mls"]} + ] +} diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/.assetsignore b/hkmc2/shared/src/test/mlscript-packages/web-ide/.assetsignore new file mode 100644 index 0000000000..fbdc486716 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/.assetsignore @@ -0,0 +1,5 @@ +*.mls +.assetsignore +.gitignore +README.md +hkmc2/ diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/.gitignore b/hkmc2/shared/src/test/mlscript-packages/web-ide/.gitignore new file mode 100644 index 0000000000..c795b054e5 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/.gitignore @@ -0,0 +1 @@ +build \ No newline at end of file diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/AGENTS.md b/hkmc2/shared/src/test/mlscript-packages/web-ide/AGENTS.md new file mode 100644 index 0000000000..188b031043 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/AGENTS.md @@ -0,0 +1,423 @@ +# Web IDE Agent Guide + +This package is the MLscript Web IDE. It is unusual in this repository because +the application source is written in MLscript, compiled to browser JavaScript, +and then served as a static web app. Most agents have not seen MLscript before: +read this file before editing anything in this package. + +## First Rules + +- Edit `.mls` files as source of truth. The adjacent `.mjs` files are generated + package outputs. +- Do not hand-edit generated `.mjs` files unless the user explicitly asks for a + generated-output investigation. If tests regenerate them, review and commit + the expected generated changes. +- Preserve indentation whitespace and blank lines. Empty lines in this + repository are often intentional. +- Keep changes scoped. This package is UI-heavy and event-driven; broad + refactors can easily break browser behavior. +- Do not display fake or mock data in production UI. If a real subsystem is not + available, show a clear empty or unavailable state. +- Do not add React, Vue, Svelte, or another frontend framework. The Web IDE uses + native custom elements, browser events, workers, dialogs, CSS, and MLscript. +- Prefer one commit per fix or small coherent UI pass. + +## Package Location + +The package root is: + +```sh +hkmc2/shared/src/test/mlscript-packages/web-ide +``` + +Run repository-level commands from the repository root unless a command +explicitly says to `cd` into this package. + +## Source Layout + +- `index.html`: static entry point. It imports generated component `.mjs` files, + `style.css`, the Lucide icon font, and `main.mjs`. +- `style.css`: all current UI styling. It owns the workbench grid, rails, + sidebars, bottom panel, dialogs, editor chrome, search, problems, logs, and + outline presentation. +- `main.mls`: browser bootstrap. It loads the compiled MLscript compiler bundle, + loads standard library files into the in-browser filesystem, creates the + default `/main.mls`, starts outline analysis, and wires compile/execute/open + events. +- `filesystem/`: in-memory and persisted browser filesystem. + - `fs.mls`: file tree operations and filesystem events. + - `persistent.mls`: local persistence for user files. +- `compiler/`: compile worker front end. + - `index.mls`: browser-side compiler worker API. + - `worker.mls`: worker-side compilation entry. +- `execution/`: sandboxed JavaScript execution. + - `runner.mls`: browser-side execution API. + - `worker.mls`: worker-side execution runtime. +- `analysis/`: outline and command-palette symbol analysis. + - `index.mls`: analysis worker lifecycle, filesystem change batching, active + file tracking, symbol index events, and test hooks. + - `worker.mls`: worker-side calls into the compiler analysis API. + - `SymbolTree.mls`: normalizes symbol documents, ranges, expansion keys, and + navigation details. +- `common/`: shared browser utilities. + - `JS.mls`: helpers for JavaScript interop and missing values. + - `Logger.mls`: centralized logging event emitter. + - `SettingsStore.mls`: `settings.`-prefixed localStorage preferences. +- `components/`: custom elements. Each file usually registers one custom element + with `customElements.define`. +- `mockWorkbenchData.mls`: legacy prototype data used by hidden/deferred mock + panels. Do not use it for newly shipped production behavior. +- `vendors/std/` and `mlscript-std/` outputs: standard library assets used by + the package tests and browser runtime. +- `build/`: ignored local compiler bundle output. It normally contains + `MLscript.mjs` copied from the Scala.js build. + +## Current UI Architecture + +The app is a static document containing ``. + +`IdeWorkbench.mls` renders the main shell: + +- top toolbar: ``; +- left activity rail and panels: Files, Search, Outline, Examples, with Source + Control currently hidden/commented; +- center editor: ``; +- right rail and panel: Problems via ``; +- bottom panel: `` for Output and Logs; +- status bar; +- native custom elements for command palette, share, Settings, and project + switcher dialogs. + +Panels are switched by rail buttons using `data-sidebar-side` and +`data-sidebar-panel`. The active panel receives the `active` class and is not +hidden. Resize handles target the active sidebar panel. + +The bottom panel follows the same mental model as the side rails: clicking the +active tab folds it, clicking a different tab switches it. The old separate +collapse button was removed. + +## Browser Event Contracts + +The package is intentionally decoupled through DOM events. Before changing a +component, search for both dispatchers and listeners. + +Important events: + +- `compile-requested`: toolbar/editor shortcut asks `main.mls` to compile the + active `.mls` file. +- `execute-requested`: toolbar/editor shortcut asks `main.mls` to execute. The + output panel should unfold before execution output appears. +- `terminate-requested`: asks the execution runner to stop. +- `open-file-at-location`: opens a file in the editor and selects the requested + line/column/range. +- `active-tab-changed`: editor announces the active file path; workbench status + and analysis listen to it. +- `cursor-position-changed`: editor announces line, column, and selection + length for the status bar. +- `compilation-status-change`: compiler status updates the toolbar/workbench. +- `execution-status-change`: runner status updates the toolbar/workbench. +- `analysis-document-updated`: outline receives the active file symbol tree. +- `analysis-status-changed`: outline receives loading/error/ready status. +- `analysis-symbol-index-updated`: command palette receives workspace symbols. +- `web-ide-log`: centralized log entries consumed by the Logs tab. +- `sidebar-toggle-requested`: components can ask the workbench to fold/unfold a + side panel. +- `new-file-requested`: command palette asks the File Explorer to start the + existing new-file flow. +- `settings-dialog-open-requested`: toolbar asks the Settings dialog to open. +- `setting-changed`: Settings dialog announces a persisted preference change. + +## Error Surfacing Contracts + +User-visible errors must not disappear into logs only. When touching compile, +execute, workers, diagnostics, toolbar status, or logs, preserve these contracts. + +Compile diagnostics: + +- ordinary compiler diagnostics are stored in the Problems sidebar through + `diagnostics-inspector.setDiagnostics(...)`; +- the Problems sidebar shows workspace-wide diagnostics by default and can scope + to the current file; +- non-`.mls` files must not be compiled by the Compile button. + +Fatal compiler failures: + +- compiler worker internal failures are reported by `compiler/index.mls` as + `compilation-status-change` with status `fatal`; +- `main.mls` converts those failures into an `internal` Problems entry with + source `compiler`; +- the center toolbar/workbench indicator must show `Fatal error`; +- the bottom panel must switch to Logging and record the fatal payload. + +Execution failures: + +- `main.mls` executes the compiled `.mjs` path but also passes the source path to + `execution/runner.mls`; +- `execution/worker.mls` should report runtime failures as structured + `runtime-error` messages with `name`, `message`, and `stack`; +- `execution/runner.mls` converts runtime failures into an `error` Problems + entry with source `execution`; +- that Problems entry should name both the culprit source file, for example + `/std/CSP.mls`, and the executed module, for example `/std/CSP.mjs`; +- Execute should unfold Output, and execution failures should be visible there + immediately as well as in centralized Logging. + +## MLscript Notes For New Agents + +MLscript here compiles to JavaScript modules, so most code uses browser globals +directly. A few syntax patterns appear everywhere: + +```mlscript +import "./common/JS.mls" + +open JS { Absent } + +module Logger with... + +fun message(text) = + "Hello " + text + +class MyElement extends HTMLElement with + let + value = null + + fun connectedCallback() = + this.render() + + fun render() = + set this.innerHTML = "
Ready
" +``` + +Common idioms: + +- `fun name(args) = ...` defines a function. +- `let` introduces local bindings. A multi-line `let` block is common before an + expression. +- `mut { key: value }` creates a mutable JavaScript-like object. +- `set target = value` mutates an existing binding or property. +- `if value is "x" then ... else ...` is pattern matching, not JavaScript + equality syntax. +- `Absent` is used for missing JavaScript fields. Many checks look like + `field is Absent then ...`. +- `~Absent as value` means "present, bind it as value". +- JavaScript property names that are reserved words are quoted, for example + `event.'type`. +- Rest parameters are written as `...body`. +- DOM APIs such as `document.querySelector`, `new CustomEvent`, `new Worker`, + `window.setTimeout`, and `customElements.define` are used directly. + +Style constraints from the repository still apply here: + +- Do not use `asInstanceOf` unless there is no reasonable alternative. +- Do not introduce default arguments in core business logic. +- Do not remove existing `end` markers in files that use them. + +## Generated Files And Build Outputs + +The package test runner compiles `.mls` files to adjacent `.mjs` files. This is +why most source files have a generated sibling: + +```text +components/OutlinePanel.mls +components/OutlinePanel.mjs +``` + +The source file is the `.mls` file. The generated `.mjs` file may change after +running the package tests. If the test runner updates generated output, inspect +the diff and commit it when it is expected. + +The browser compiler bundle is separate: + +```text +build/MLscript.mjs +build/MLscript.mjs.map +``` + +This directory is ignored local runtime output. To refresh it for a browser +preview, run the Scala.js build and copy the output: + +```sh +sbt --client hkmc2JS/fullOptJS +cp hkmc2/js/target/scala-3.8.3/hkmc2-opt/MLscript.mjs \ + hkmc2/shared/src/test/mlscript-packages/web-ide/build/MLscript.mjs +cp hkmc2/js/target/scala-3.8.3/hkmc2-opt/MLscript.mjs.map \ + hkmc2/shared/src/test/mlscript-packages/web-ide/build/MLscript.mjs.map +``` + +Use the exact Scala version path present in the local build if it changes. + +## CLI Test Workflow + +Prefer `sbt --client` after the sbt server is running. SBT startup is slow. + +For most Web IDE-only changes, run: + +```sh +sbt --client "hkmc2PackagesTest/testOnly hkmc2.PackageTestRunner -- -z web-ide" +``` + +This compiles the `web-ide` MLscript package and runs the focused package test +suite. It is the default check for component, style, package, analysis, compiler +front-end, and UI event changes. + +For broader compiler/runtime changes, first make sure runtime test assets are +generated: + +```sh +sbt --client hkmc2JVM/test +``` + +Before finishing a substantial or risky change, run the full test suite: + +```sh +sbt --client hkmc2AllTests/test +``` + +If golden or generated test output changes, inspect it and commit the expected +updates. CI can fail when generated test output is stale. + +Always run diff hygiene before committing: + +```sh +git diff --check +git status --short +``` + +Unrelated dirty files are common in this repository. Do not revert them. + +## Browser Verification Workflow + +CLI tests are not enough for UI work. Use a browser check for any change that +affects layout, custom elements, dialogs, sidebars, panels, editor interaction, +search, outline, diagnostics, logs, compile, execute, or worker behavior. + +If the user has an existing preview server, use it. In recent UI work the shared +preview has often been: + +```text +http://127.0.0.1:3003/index.html +``` + +Do not start another server when the user says a preview server is already +running. + +The Web IDE preview workflow deploys to Cloudflare only when both +`CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` are configured in the +repository where the workflow runs. Forks need their own Actions secrets; the +upstream repository's secrets are not inherited by fork push workflows. + +If no server exists and the user has not prohibited starting one, serve the +package root: + +```sh +cd hkmc2/shared/src/test/mlscript-packages/web-ide +python3 -m http.server 8123 --bind 127.0.0.1 +``` + +Then open: + +```text +http://127.0.0.1:8123/index.html? +``` + +Browser smoke checks for most UI changes: + +- page title is `MLscript Web IDE`; +- page loads with no browser console errors; +- Files panel opens and can open `/main.mls`; +- editor content is scrollable; +- left and right rails can show, hide, and switch panels; +- resize handles only affect the visible side panel area; +- Compile is disabled when there is no active file and skips non-`.mls` files; +- Compile on `/main.mls` updates Problems with real diagnostics or an empty + state; +- fatal compiler failures show `Fatal error` in the toolbar, populate Problems + as `Internal`, and switch the bottom panel to Logging; +- Execute unfolds Output and shows output in order; +- execution runtime errors, including std-file execution failures such as + `/std/CSP.mls`, populate Problems with the culprit source path and executed + module path; +- Logs tab receives centralized log entries and auto-scrolls when appropriate; +- Search results are grouped by file, highlight the selected occurrence, fold by + file, and open the exact occurrence; +- Outline shows real symbols or a clear unavailable state, never mock symbols; +- command palette is centered, keyboard navigable, and keeps the input fixed; +- share dialog remains centered and provides the configured download buttons; +- desktop and narrow viewports do not horizontally overflow. + +Use browser-side test hooks where available: + +- `window.__mlscriptAnalysis.analyzeNow()`; +- `window.__mlscriptAnalysis.injectResponse(response)`; +- `window.__mlscriptAnalysis.symbolIndex()`; +- `analysis-test-inject-response` events for analysis worker scenarios. + +When using Playwright tools, remove `.playwright-mcp/` artifacts before +committing unless the user explicitly asks to keep screenshots or snapshots. + +## Implementation Guidelines + +### Custom Elements + +Each component should own its DOM subtree and event listeners. Keep the public +surface small: methods such as `setDiagnostics`, `showOutput`, or `openFileAtLine` +are acceptable when another component needs a direct imperative entrypoint. + +Use native HTML where practical: + +- `` for modal dialogs; +- ` + + + +
+ """ + activeContentHtml() + """ +
+ """ + this.classList.toggle("collapsed", isCollapsed) + attachEventListeners() + scrollOutputToLatest() + scrollLoggingToLatest() + + fun checkedAttr() = + if preserveLogs then " checked" else "" + + fun selectedAttr(value, selectedValue) = + if value === selectedValue then " selected" else "" + + fun logLevelOptionHtml(value, label) = + """""" + + fun sourceOptionsHtml() = + let + sources = mut [] + html = """""" + logEntries.forEach of (entry, ...) => + if not sources.includes(entry.source) do sources.push(entry.source) + if logSourceFilter !== "all" and not sources.includes(logSourceFilter) do sources.push(logSourceFilter) + sources.sort() + sources.forEach of (source, ...) => + set html += """""" + html + + fun logFilterControlsHtml() = + if activeTab !== "logging" then "" + else """ +
+ + + +
+ """ + + fun tabButtonHtml(id, label, icon) = + let + active = activeTab === id and not isCollapsed + activeClass = if active then " active" else "" + selected = if active then "true" else "false" + tabIndex = if activeTab === id then "0" else "-1" + """ + + """ + + fun activeContentHtml() = if activeTab is + "logging" then loggingHtml() + "terminal" then terminalHtml() + else outputHtml() + + fun outputHtml() = + let html = """
""" + set html += feedbackHtml() + if messages.length is 0 then + set html += emptyHtml("Output is empty") + else + messages.forEach of (entry, ...) => + set html += messageRowHtml(entry) + set html += """
""" + html + + fun loggingHtml() = + let html = """
""" + let visibleEntries = visibleLogEntries() + set html += feedbackHtml() + if logEntries.length is 0 then + set html += emptyHtml("Logging buffer is empty") + else if visibleEntries.length is 0 then + set html += emptyHtml("No log entries match the filters") + else + visibleEntries.forEach of (entry, ...) => + set html += logEntryHtml(entry) + set html += """
""" + html + + fun terminalHtml() = + let html = """
""" + if terminalLines.length is 0 then + set html += emptyHtml("Terminal is empty") + else + set html += """
""" + escapeHtml(terminalLines.join("\n")) + """
""" + set html += """ +
+ $ + + +
+ """ + set html += feedbackHtml() + set html += """
""" + html + + fun emptyHtml(label) = + """
""" + label + """
""" + + fun feedbackHtml() = + if feedbackText === "" then "" + else """""" + + fun message(kind, args) = + mut { :kind, content: args, timestamp: new Date() } + + fun log(kind, ...args) = + messages.push(message(kind, args)) + addLogEntry(mut + level: outputLogLevel(kind) + timestamp: new Date().toISOString() + source: "Output" + message: formattedArgs(args) + body: args + ) + if activeTab === "output" do render() + + fun outputLogLevel(kind) = if kind is + "error" then "error" + "warn" then "warn" + "info" then "info" + else "debug" + + fun stringifyArg(arg) = if typeof(arg) is + "object" then + js.try_catch( + () => JSON.stringify(arg, null, 2) + error => String(arg) + ) + else String(arg) + + fun formattedArgs(args) = + args.map(arg => stringifyArg(arg)).join(" ") + + fun formattedContent(message) = + formattedArgs(message.content) + + fun valueOrDefault(value, fallback) = if value is + Absent then fallback + null then fallback + else value + + fun normalizeLevel(level) = if String(valueOrDefault(level, "info")) is + "error" then "error" + "warn" then "warn" + "debug" then "debug" + "trace" then "trace" + else "info" + + fun normalizeBody(body) = if body is + Absent then [] + null then [] + else Array.from(body) + + fun normalizeLogEntry(entry) = + let body = normalizeBody(entry.body) + mut + level: normalizeLevel(entry.level) + timestamp: String(valueOrDefault(entry.timestamp, new Date().toISOString())) + source: String(valueOrDefault(entry.source, "Web IDE")) + message: String(valueOrDefault(entry.message, formattedArgs(body))) + body: body + + fun addLogEntry(entry) = + logEntries.push(normalizeLogEntry(entry)) + while logEntries.length > logBufferLimit do logEntries.shift() + if activeTab === "logging" do render() + + fun logTimestampValue(entry) = + let date = new Date(entry.timestamp) + if Number.isFinite(date.getTime()) then date.getTime() else 0 + + fun logSourceMatches(entry) = + if + logSourceFilter === "all" then true + else entry.source === logSourceFilter + + fun logEntryMatches(entry) = + if + logLevelFilter === "all" then logSourceMatches(entry) + entry.level === logLevelFilter then logSourceMatches(entry) + else false + + fun visibleLogEntries() = + let entries = logEntries.filter(entry => logEntryMatches(entry)) + entries.sort((a, b) => if + logSortOrder === "desc" then logTimestampValue(b) - logTimestampValue(a) + else logTimestampValue(a) - logTimestampValue(b) + ) + entries + + fun iconClassName(kind) = if kind is + "error" then "icon-x" + "warn" then "icon-triangle-alert" + "info" then "icon-info" + else "icon-chevron-right" + + fun logLevelClass(level) = if level is + "error" then "log-level-error" + "warn" then "log-level-warn" + "debug" then "log-level-debug" + "trace" then "log-level-trace" + else "log-level-info" + + fun logIconClassName(level) = if level is + "error" then "icon-x" + "warn" then "icon-triangle-alert" + "debug" then "icon-bug" + "trace" then "icon-route" + else "icon-info" + + fun logBodyText(entry) = + entry.body.map(arg => stringifyArg(arg)).join("\n") + + fun isWhitespaceChar(ch) = if ch is + " " then true + "\n" then true + "\r" then true + "\t" then true + else false + + fun compactLogText(text) = + let + compact = "" + pendingSpace = false + i = 0 + while i < text.length do + let ch = text.charAt(i) + if isWhitespaceChar(ch) then + if compact !== "" do set pendingSpace = true + else + if pendingSpace do + set compact += " " + set pendingSpace = false + set compact += ch + set i += 1 + if compact.length > 96 then compact.slice(0, 95) + "…" else compact + + fun logDate(timestamp) = + let date = new Date(timestamp) + if Number.isFinite(date.getTime()) then date else null + + fun formatLogTime(timestamp) = + let date = logDate(timestamp) + if date is + null then String(timestamp) + else date.toLocaleTimeString(undefined, + hour: "2-digit" + minute: "2-digit" + ) + + fun formatLogDateTime(timestamp) = + let date = logDate(timestamp) + if date is + null then String(timestamp) + else date.toLocaleString(undefined, + year: "numeric" + month: "short" + day: "numeric" + hour: "2-digit" + minute: "2-digit" + second: "2-digit" + ) + + fun logEntryRowHtml(entry, bodyText, rowTag) = + let preview = compactLogText(bodyText) + let bodyPreview = if preview === "" then "" + else """""" + escapeHtml(preview) + """""" + """ + <""" + rowTag + """ class="log-entry-row"> + + """ + entry.level.toUpperCase() + """ + + """ + escapeHtml(entry.source) + """ + """ + escapeHtml(entry.message) + """ + """ + bodyPreview + """ + + """ + + fun logBodyHtml(entry) = + let text = logBodyText(entry) + if text === "" then "" + else """ +
+ """ + logEntryRowHtml(entry, text, "summary") + """ +
""" + escapeHtml(text) + """
+
+ """ + + fun logEntryText(entry) = + let header = "[" + formatLogDateTime(entry.timestamp) + "] " + entry.level.toUpperCase() + " " + entry.source + " - " + entry.message + let body = logBodyText(entry) + if body === "" then header else header + "\n" + body + + fun logEntryHtml(entry) = + let body = logBodyText(entry) + if body === "" then """ +
+ """ + logEntryRowHtml(entry, body, "div") + """ +
+ """ + else """ +
+ """ + logBodyHtml(entry) + """ +
+ """ + + fun messageRowHtml(message) = + """ +
+ + """ + escapeHtml(formattedContent(message)) + """ +
+ """ + + fun clear() = if preserveLogs then + set feedbackText = "Preserve logs is on" + render() + else + if activeTab is + "logging" then + let hadEntries = logEntries.length > 0 + set + logEntries = mut [] + feedbackText = if hadEntries then "Logging buffer cleared" else "" + "terminal" then + set + terminalLines = mut [] + feedbackText = "Terminal cleared" + else + let hadMessages = messages.length > 0 + set + messages = mut [] + feedbackText = if hadMessages then "Output cleared" else "" + render() + + fun setPreserveLogs(value) = + set preserveLogs = value + render() + + fun normalizeLogLevelFilter(value) = if String(value) is + "error" then "error" + "warn" then "warn" + "info" then "info" + "debug" then "debug" + "trace" then "trace" + else "all" + + fun setLogLevelFilter(value) = + set + logLevelFilter = normalizeLogLevelFilter(value) + feedbackText = "" + PanelPersistence.saveString("logs-level-filter", logLevelFilter) + render() + + fun setLogSourceFilter(value) = + set + logSourceFilter = if String(value) === "" then "all" else String(value) + feedbackText = "" + PanelPersistence.saveString("logs-source-filter", logSourceFilter) + render() + + fun setLogSortOrder(value) = + set + logSortOrder = if String(value) === "desc" then "desc" else "asc" + feedbackText = "" + render() + + fun setActiveTab(tabName) = if tabName is + "output" | "logging" | "terminal" then + PanelPersistence.saveString("bottom-panel-active-tab", tabName) + if tabName === activeTab then + toggleCollapse() + else + set + activeTab = tabName + feedbackText = "" + if isCollapsed do + set isCollapsed = false + restoreSavedSize() + render() + else () + + fun tabLabel(tabName) = if tabName is + "logging" then "Logging" + "terminal" then "Terminal" + else "Output" + + fun visibleText() = if activeTab is + "logging" then visibleLogEntries().map(entry => logEntryText(entry)).join("\n\n") + "terminal" then terminalLines.join("\n") + else messages.map(entry => formattedContent(entry)).join("\n") + + fun downloadVisibleContent() = + let + blob = new! window.Blob([visibleText()], mut { 'type: "text/plain" }) + url = window.URL.createObjectURL(blob) + link = document.createElement("a") + set + link.href = url + link.download = "mlscript-" + activeTab + ".txt" + link.style.display = "none" + document.body.appendChild(link) + link.click() + link.remove() + window.URL.revokeObjectURL(url) + set feedbackText = "Downloaded " + tabLabel(activeTab) + render() + + fun runTerminalCommand(command) = + let trimmed = command.trim() + if trimmed !== "" do + terminalLines.push("$ " + trimmed) + terminalLines.push("Command queued in the mock terminal") + set feedbackText = "" + render() + + fun valueOrEmpty(value) = if value is + Absent then "" + present then present + + fun saveCurrentSize() = + set + this.dataset.lastHeight = valueOrEmpty(this.style.height) + this.dataset.lastMinHeight = valueOrEmpty(this.style.minHeight) + this.dataset.lastMaxHeight = valueOrEmpty(this.style.maxHeight) + this.style.height = "40px" + this.style.minHeight = "40px" + this.style.maxHeight = "40px" + this.style.flexBasis = "40px" + this.style.flexGrow = "0" + this.style.flexShrink = "0" + + fun restoreSavedSize() = + let + lastHeight = this.dataset.lastHeight + lastMinHeight = this.dataset.lastMinHeight + lastMaxHeight = this.dataset.lastMaxHeight + if lastHeight is + Absent then + resetInlineSize() + "" then + resetInlineSize() + "40px" then + resetInlineSize() + savedHeight then + set + this.style.height = savedHeight + this.style.minHeight = valueOrEmpty(lastMinHeight) + this.style.maxHeight = valueOrEmpty(lastMaxHeight) + this.style.flexBasis = savedHeight + + fun resetInlineSize() = + set + this.style.height = "" + this.style.minHeight = "" + this.style.maxHeight = "" + this.style.flexBasis = "" + this.style.flexGrow = "" + this.style.flexShrink = "" + + fun toggleCollapse() = + set isCollapsed = not isCollapsed + if + isCollapsed then saveCurrentSize() + else restoreSavedSize() + render() + + fun shouldExpand() = if + isCollapsed then true + else this.classList.contains("collapsed") + + fun expand() = if shouldExpand() do + set isCollapsed = false + restoreSavedSize() + render() + + fun showOutput() = + set activeTab = "output" + PanelPersistence.saveString("bottom-panel-active-tab", activeTab) + expand() + if not shouldExpand() do render() + + fun showLogging() = + set activeTab = "logging" + PanelPersistence.saveString("bottom-panel-active-tab", activeTab) + expand() + if not shouldExpand() do render() + + fun clearOutput() = + set activeTab = "output" + PanelPersistence.saveString("bottom-panel-active-tab", activeTab) + clear() + + fun clearLogs() = + set activeTab = "logging" + PanelPersistence.saveString("bottom-panel-active-tab", activeTab) + clear() + + fun scrollLoggingToLatest() = + if activeTab === "logging" and not isCollapsed do + if this.querySelector(".bottom-panel-content") is ~null as content do + window.requestAnimationFrame(() => if + logSortOrder === "desc" then set content.scrollTop = 0 + else set content.scrollTop = content.scrollHeight + ) + + fun scrollOutputToLatest() = + if activeTab === "output" and not isCollapsed do + if this.querySelector(".bottom-panel-content") is ~null as content do + window.requestAnimationFrame(() => set content.scrollTop = content.scrollHeight) + + fun attachEventListeners() = + let panel = this + this.querySelectorAll(".bottom-panel-tab").forEach of (button, ...) => + button.addEventListener("click", () => panel.setActiveTab(button.dataset.bottomTab)) + if this.querySelector(".clear-btn") is ~null as clearBtn do + clearBtn.addEventListener("click", () => panel.clear()) + if this.querySelector(".download-btn") is ~null as downloadBtn do + downloadBtn.addEventListener("click", () => panel.downloadVisibleContent()) + if this.querySelector(".log-level-filter") is ~null as levelFilter do + levelFilter.addEventListener("change", event => panel.setLogLevelFilter(event.target.value)) + if this.querySelector(".log-source-filter") is ~null as sourceFilter do + sourceFilter.addEventListener("change", event => panel.setLogSourceFilter(event.target.value)) + if this.querySelector(".log-sort-order") is ~null as sortOrder do + sortOrder.addEventListener("change", event => panel.setLogSortOrder(event.target.value)) + if this.querySelector(".preserve-logs-checkbox") is ~null as preserveLogsCheckbox do + preserveLogsCheckbox.addEventListener("change", event => panel.setPreserveLogs(event.target.checked)) + if this.querySelector(".terminal-form") is ~null as form do + form.addEventListener of "submit", event => + event.preventDefault() + if form.querySelector(".terminal-input") is ~null as input do + panel.runTerminalCommand(input.value) + + fun escapeHtml(text) = + let div = document.createElement("div") + set div.textContent = text + div.innerHTML + +customElements.define("bottom-panel", BottomPanel) diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/components/DiagnosticsInspector.mls b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/DiagnosticsInspector.mls new file mode 100644 index 0000000000..ea474388a6 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/DiagnosticsInspector.mls @@ -0,0 +1,475 @@ +import "../filesystem/fs.mls" +import "./PanelPersistence.mls" +import "../common/JS.mls" +import "../common/SettingsStore.mls" + +open JS { Absent } + +class DiagnosticsInspector extends HTMLElement with + let + mode = "workspace" + diagnosticsData = [] + activeFilePath = null + enabledKinds = new Set(["error", "warning", "internal", "info"]) + expandedKeys = new Set() + + fun connectedCallback() = + this.restoreModeFromStorage() + this.render() + this.restoreSizeFromStorage() + this.attachDocumentListeners() + + fun restoreSizeFromStorage() = + let savedWidth = localStorage.getItem("diagnostics-inspector-width") + if savedWidth is ~Absent and savedWidth !== "" do + let width = parseInt(savedWidth, 10) + if width >= 150 and width <= 600 do + if document.querySelector(".app-container") is ~null as container do + container.style.setProperty("--ide-right-panel-width", width + "px") + this.setAttribute("width", width) + + fun saveSizeToStorage(width) = + localStorage.setItem("diagnostics-inspector-width", width) + + fun normalizeMode(value) = if value is + "current" then "current" + else "workspace" + + fun restoreModeFromStorage() = + let defaultMode = SettingsStore.getSetting("problems.defaultScope", "workspace") + set mode = normalizeMode(PanelPersistence.restoreString("diagnostics-inspector-mode", defaultMode)) + + fun setDiagnostics(diagnosticsPerFile) = + let nextData = if diagnosticsPerFile is + Absent then [] + null then [] + files then files + set + diagnosticsData = nextData + enableKindsForDiagnostics(nextData) + this.render() + + fun enableKindsForDiagnostics(files) = + if files is ~Absent do + files.forEach of (fileData, ...) => + if fileData.diagnostics is ~Absent as diagnostics do + diagnostics.forEach of (diagnostic, ...) => + enabledKinds.add(diagnosticKind(diagnostic)) + + fun activeFileFromDetail(detail) = + if + detail is Absent then null + detail.path is Absent then null + else detail.path + + fun setActiveFile(detail) = + set activeFilePath = activeFileFromDetail(detail) + if mode === "current" do this.render() + + fun setOutput(message) = + let text = if message is + Absent then "Worker error" + else message + set + diagnosticsData = runtimeDiagnosticData(text) + this.render() + + fun runtimeDiagnosticData(message) = + [ + mut + path: "/runtime" + diagnostics: [ + mut + kind: "internal" + source: "runtime" + mainMessage: message + excerpt: "Worker runtime error" + line: 1 + allMessages: [ + mut + messageBits: [mut { text: message }] + location: mut { start: 0, end: 0 } + ] + ] + ] + + fun render() = + let + allEntries = diagnosticEntries(diagnosticsData) + scopedEntries = scopedDiagnosticEntries(allEntries) + entries = filteredDiagnosticEntries(scopedEntries) + set this.innerHTML = """ +
+
+

Problems

+
+ """ + countsHtml(scopedEntries) + """ +
+ """ + modeButtonHtml("workspace", "Workspace", "icon-list") + """ + """ + modeButtonHtml("current", "Current file", "icon-file-code") + """ +
+
+
""" + contentHtml(entries) + """
+ """ + footerHtml(entries) + """ + """ + attachEventListeners() + + fun modeButtonHtml(value, label, icon) = + let + activeClass = if mode === value then " active" else "" + pressed = if mode === value then "true" else "false" + """ + + """ + + fun countsHtml(entries) = + """ +
+ """ + countPillHtml("error", "Errors", entries) + """ + """ + countPillHtml("warning", "Warnings", entries) + """ + """ + countPillHtml("internal", "Internal", entries) + """ + """ + countPillHtml("info", "Info", entries) + """ +
+ """ + + fun countPillHtml(kind, label, entries) = + let activeClass = if enabledKinds.has(kind) then " active" else "" + let pressed = if enabledKinds.has(kind) then "true" else "false" + """ + + """ + + fun countKind(entries, kind) = + let count = 0 + entries.forEach of (entry, ...) => + if diagnosticKind(entry.diagnostic) === kind do set count += 1 + count + + fun contentHtml(entries) = if + entries.length === 0 then emptyHtml() + else listHtml(entries) + + fun emptyHtml() = + """ +
+ + No problems +
+ """ + + fun listHtml(entries) = + let html = """
""" + set html += entries.map(entry => listRowHtml(entry)).join("") + set html += """
""" + html + + fun listRowHtml(entry) = + let + kind = diagnosticKind(entry.diagnostic) + location = entryLocation(entry) + expanded = expandedKeys.has(entry.key) + expandedClass = if expanded then " expanded" else "" + expandedAttr = if expanded then "true" else "false" + """ +
+ + """ + expandedDetailHtml(entry, expanded) + """ +
+ """ + + fun expandedDetailHtml(entry, expanded) = + if not expanded then "" + else sourceCardHtml(entry) + + fun sourceCardHtml(entry) = + let + kind = diagnosticKind(entry.diagnostic) + location = entryLocation(entry) + key = escapeHtml(entry.key) + """ +
+
+ +
+

""" + escapeHtml(diagnosticMainMessage(entry.diagnostic)) + """

+ """ + escapeHtml(entry.filePath) + ":" + location.line + ":" + location.column + """ Β· """ + escapeHtml(capitalize(diagnosticSource(entry.diagnostic))) + """ +
+
+
+ """ + location.line + """ +
""" + highlightedSourceHtml(entry) + """
+
+ """ + messageHtml(entry.diagnostic) + """ +
+ + +
+
+ """ + + fun messageHtml(diagnostic) = + """ +

""" + escapeHtml(diagnosticDetailText(diagnostic)) + """

+ """ + + fun diagnosticEntries(files) = + let entries = mut [] + if files is ~Absent do + files.forEach of (fileData, fileIndex) => + if fileData.diagnostics is ~Absent as diagnostics do + diagnostics.forEach of (diagnostic, diagnosticIndex) => + let + filePath = filePathOf(fileData) + key = diagnosticKey(filePath, fileIndex, diagnosticIndex, diagnostic) + entries.push(mut + filePath: filePath + diagnostic: diagnostic + fileIndex: fileIndex + diagnosticIndex: diagnosticIndex + key: key + ) + entries + + fun scopedDiagnosticEntries(entries) = + if mode is + "current" then entries.filter(entry => activeFilePath !== null and entry.filePath === activeFilePath) + else entries + + fun filteredDiagnosticEntries(entries) = + entries.filter(entry => enabledKinds.has(diagnosticKind(entry.diagnostic))) + + fun filePathOf(fileData) = if fileData.path is + Absent then "Unknown file" + path then path + + fun diagnosticKey(filePath, fileIndex, diagnosticIndex, diagnostic) = + filePath + "::" + fileIndex + ":" + diagnosticIndex + ":" + diagnosticMainMessage(diagnostic) + + fun entryByKey(key) = + let found = null + diagnosticEntries(diagnosticsData).forEach of (entry, ...) => + if found is null and entry.key === key do set found = entry + found + + fun diagnosticKind(diagnostic) = + let kind = if diagnostic.kind is + Absent then "info" + value then value + if kind is + "error" then "error" + "warning" then "warning" + "internal" then "internal" + else "info" + + fun diagnosticSource(diagnostic) = if diagnostic.source is + Absent then "compiler" + source then source + + fun diagnosticMainMessage(diagnostic) = if diagnostic.mainMessage is + Absent then firstDiagnosticText(diagnostic) + message then message + + fun firstDiagnosticText(diagnostic) = if + diagnostic.allMessages is Absent then "Diagnostic" + diagnostic.allMessages.length === 0 then "Diagnostic" + else messageText(diagnostic.allMessages.at(0)) + + fun diagnosticDetailText(diagnostic) = + let text = "" + if diagnostic.allMessages is ~Absent as messages do + messages.forEach of (message, ...) => + let content = messageText(message) + if content !== "" do + if text === "" then set text = content + else set text += " " + content + if text === "" then diagnosticMainMessage(diagnostic) else text + + fun diagnosticMarkdown(entry) = + let location = entryLocation(entry) + "### " + diagnosticMainMessage(entry.diagnostic) + "\n\n" + + "- File: `" + entry.filePath + "`\n" + + "- Location: " + location.line + ":" + location.column + "\n" + + "- Severity: " + severityLabel(diagnosticKind(entry.diagnostic)) + "\n" + + "- Source: " + capitalize(diagnosticSource(entry.diagnostic)) + "\n\n" + + diagnosticDetailText(entry.diagnostic) + "\n\n" + + "```mlscript\n" + + entryExcerpt(entry) + "\n" + + "```\n" + + fun messageText(message) = + let text = "" + if message.messageBits is ~Absent as bits do + bits.forEach of (bit, ...) => + if bit.text is ~Absent as bitText do set text += bitText + if bit.code is ~Absent as bitCode do set text += bitCode + text + + fun primaryLocation(diagnostic) = + let result = null + if diagnostic.allMessages is ~Absent as messages do + messages.forEach of (message, ...) => + if result is null do + if message.location is ~Absent as location do set result = location + result + + fun entryLocation(entry) = if entry.diagnostic.line is + Absent then computedLocation(entry) + line then lineColumn(line, 1) + + fun computedLocation(entry) = + let location = primaryLocation(entry.diagnostic) + if location is + null then lineColumn(1, 1) + else getLineAndColumn(sourceText(entry.filePath), location.start) + + fun lineColumn(line, column) = + mut { :line, :column } + + fun getLineAndColumn(text, offset) = + let lines = text.substring(0, offset).split("\n") + lineColumn(lines.length, lines.at(lines.length - 1).length + 1) + + fun sourceText(filePath) = if fs.read(filePath) is + Absent then "" + text then text + + fun entryExcerpt(entry) = if entry.diagnostic.excerpt is + Absent then sourceLine(entry) + excerpt then excerpt + + fun sourceLine(entry) = + let + location = entryLocation(entry) + lines = sourceText(entry.filePath).split("\n") + if location.line <= lines.length then lines.at(location.line - 1) + else diagnosticMainMessage(entry.diagnostic) + + fun highlightedSourceHtml(entry) = + let + text = entryExcerpt(entry) + location = primaryLocation(entry.diagnostic) + if location is + null then escapeHtml(text) + else + let + source = sourceText(entry.filePath) + lineStart = sourceLineStartOffset(source, location.start) + startColumn = Math.max(0, location.start - lineStart) + endColumn = Math.max(startColumn, location.end - lineStart) + if startColumn >= text.length then escapeHtml(text) + else + let + boundedEnd = Math.min(text.length, endColumn) + before = text.slice(0, startColumn) + selected = text.slice(startColumn, boundedEnd) + after = text.slice(boundedEnd) + escapeHtml(before) + """""" + escapeHtml(selected) + """""" + escapeHtml(after) + + fun sourceLineStartOffset(text, offset) = + let index = text.lastIndexOf("\n", Math.max(0, offset - 1)) + if index < 0 then 0 else index + 1 + + fun fileCount(entries) = + let files = new Set() + entries.forEach of (entry, ...) => files.add(entry.filePath) + files.size + + fun footerHtml(entries) = + let problemLabel = if entries.length is 1 then "problem" else "problems" + let fileLabel = if fileCount(entries) is 1 then "file" else "files" + """ + + """ + + fun setMode(nextMode) = if nextMode is + "workspace" | "current" then + set mode = nextMode + PanelPersistence.saveString("diagnostics-inspector-mode", mode) + render() + else () + + fun toggleKind(kind) = + if enabledKinds.has(kind) then enabledKinds.delete(kind) + else enabledKinds.add(kind) + render() + + fun toggleEntry(key) = + if expandedKeys.has(key) then expandedKeys.delete(key) + else expandedKeys.add(key) + render() + + fun dispatchOpenEntry(key) = + if entryByKey(key) is ~null as entry do + let + location = entryLocation(entry) + detail = mut { filePath: entry.filePath, line: location.line } + options = mut { :detail } + document.dispatchEvent(new CustomEvent("open-file-at-location", options)) + + fun copyEntryMarkdown(key, button) = + if entryByKey(key) is ~null as entry do + window.navigator.clipboard.writeText(diagnosticMarkdown(entry)) + if button is ~Absent do + let label = button.querySelector("span") + if label is ~null as labelElement do + let previousText = labelElement.textContent + set labelElement.textContent = "Copied!" + window.setTimeout(() => set labelElement.textContent = previousText, 1000) + + fun attachEventListeners() = + let panel = this + this.querySelectorAll(".diagnostics-mode-button").forEach of (button, ...) => + button.addEventListener("click", () => panel.setMode(button.dataset.diagnosticsMode)) + this.querySelectorAll(".diagnostics-count").forEach of (button, ...) => + button.addEventListener("click", () => panel.toggleKind(button.dataset.countKind)) + this.querySelectorAll(".diagnostics-row-summary").forEach of (button, ...) => + button.addEventListener("click", () => panel.toggleEntry(button.dataset.diagnosticKey)) + this.querySelectorAll(".diagnostics-open-location").forEach of (button, ...) => + button.addEventListener("click", () => panel.dispatchOpenEntry(button.dataset.diagnosticKey)) + this.querySelectorAll(".diagnostics-copy-markdown").forEach of (button, ...) => + button.addEventListener("click", () => panel.copyEntryMarkdown(button.dataset.diagnosticKey, button)) + + fun attachDocumentListeners() = + let panel = this + document.addEventListener("active-tab-changed", event => panel.setActiveFile(event.detail)) + + fun severityLabel(kind) = if kind is + "error" then "Errors" + "warning" then "Warnings" + "internal" then "Internal" + else "Info" + + fun kindIcon(kind) = if kind is + "error" then "icon-circle-x" + "warning" then "icon-triangle-alert" + "internal" then "icon-bug" + else "icon-circle-alert" + + fun capitalize(text) = + text.charAt(0).toUpperCase() + text.slice(1) + + fun escapeHtml(text) = + let div = document.createElement("div") + set div.textContent = text + div.innerHTML + +customElements.define("diagnostics-inspector", DiagnosticsInspector) diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/components/EditorPanel.mls b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/EditorPanel.mls new file mode 100644 index 0000000000..1ddde59767 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/EditorPanel.mls @@ -0,0 +1,643 @@ +import "../filesystem/fs.mls" +import "../filesystem/projects.mls" +import "../editor/editor.mls" +import "./FileTooltip.mls" as fileTooltipModule +import "../common/SettingsStore.mls" +import "../common/JS.mls" +import "../common/Logger.mls" + +open JS { Absent } + +class EditorPanel extends HTMLElement with + set + this.openTabs = new Map() + this.activeTabId = null + this.tabCounter = 0 + this.tabTooltipTimeout = null + this.tabTooltipPendingTarget = null + this.tabTooltipHoverTarget = null + this.unsubscribe = null + this.resizeObserver = null + this.updateArrowVisibility = null + this.suppressOpenTabsPersistence = false + this.emitOpenFilesChange() + + fun connectedCallback() = + this.render() + this.setupEventListeners() + this.setupTabBarScrolling() + let panel = this + set this.unsubscribe = fs.subscribe(event => panel.handleFsEvent(event), undefined) + + fun disconnectedCallback() = + if this.unsubscribe is ~Absent as unsubscribe do + unsubscribe() + if this.resizeObserver is ~Absent as resizeObserver do + resizeObserver.disconnect() + Array.from(this.openTabs.values()).forEach of (tab, ...) => + if tab.editorView is ~Absent as editorView do + editorView.destroy() + + fun render() = + set this.innerHTML = """ +
+ +
+ + +
+
+
Open a file to start editing
+
+ """ + + fun pathsDetail(paths) = + mut { :paths } + + fun eventOptions(detail) = + mut { :detail } + + fun bubbleEventOptions(detail) = + mut { bubbles: true, :detail } + + fun sidebarToggleDetail(side, panel) = + mut { :side, :panel } + + fun dispatchSidebarToggle(side, panel) = + document.dispatchEvent(new CustomEvent("sidebar-toggle-requested", eventOptions(sidebarToggleDetail(side, panel)))) + + fun emitOpenFilesChange() = + let paths = Array.from(this.openTabs.values()).map((tab, ...) => tab.path) + document.dispatchEvent(new CustomEvent("open-files-changed", eventOptions(pathsDetail(paths)))) + + fun openTabsSettingKey() = + "editor.openTabs." + projects.currentId() + + fun activeTabPath() = + if activeTab() is + Absent then null + tab then tab.path + + fun openTabsSnapshot() = + mut + paths: Array.from(this.openTabs.values()).map((tab, ...) => tab.path) + activePath: activeTabPath() + + fun persistOpenTabs() = + if not this.suppressOpenTabsPersistence do + SettingsStore.setSetting(openTabsSettingKey(), JSON.stringify(openTabsSnapshot())) + + fun savedOpenTabsRecord() = + let raw = SettingsStore.getSetting(openTabsSettingKey(), "") + if raw === "" then null + else js.try_catch( + () => + let parsed = JSON.parse(raw) + if Array.isArray(parsed) then mut { paths: parsed, activePath: null } + else if parsed is Object then parsed + else null + error => + Logger.warn("Editor", "Ignored invalid open-tabs snapshot", error) + null + ) + + fun savedOpenPaths(record) = + if + record is Absent then [] + Array.isArray(record.paths) then record.paths + else [] + + fun savedActivePath(record) = + if + record is Absent then null + typeof(record.activePath) is "string" then record.activePath + else null + + fun restorableFile(path) = + if typeof(path) is ~"string" then false + else + let node = fs.stat(path) + if node is Absent then false + else node.'type === "file" + + fun restoreOpenTabsForCurrentProject() = + if this.openTabs.size > 0 then () + else if savedOpenTabsRecord() is ~null as snapshot do + let + seen = new Set() + paths = savedOpenPaths(snapshot) + activePath = savedActivePath(snapshot) + set this.suppressOpenTabsPersistence = true + paths.forEach of (path, ...) => + if not seen.has(path) and restorableFile(path) do + seen.add(path) + this.openFile(path, path.split("/").pop()) + if activePath is ~null and restorableFile(activePath) do + if existingTabIdFor(activePath) is ~Absent as activeTabId do + this.switchTab(activeTabId) + set this.suppressOpenTabsPersistence = false + this.updateDisplay() + + fun scheduleOpenTabsRestore() = + window.setTimeout(() => this.restoreOpenTabsForCurrentProject(), 0) + + fun closeAllTabs() = + let panel = this + Array.from(this.openTabs.keys()).forEach of (tabId, ...) => panel.closeTab(tabId) + + fun destroyTab(tab) = + if tab.editorView is ~Absent as editorView do + editorView.destroy() + tab.editorDiv.remove() + + fun resetTabsAfterFsReset() = + set this.suppressOpenTabsPersistence = true + Array.from(this.openTabs.values()).forEach of (tab, ...) => destroyTab(tab) + this.openTabs.clear() + set this.activeTabId = null + this.updateDisplay() + set this.suppressOpenTabsPersistence = false + + fun shouldHandleFsEvent(kind) = + kind is ("write" | "delete" | "rename" | "readonly" | "attr") + + fun nodeAttrs(event) = if + event.node is Absent then mut {} + event.node.attrs is Absent then mut {} + else event.node.attrs + + fun updateTabContent(tab, path) = + let + currentContent = tab.editorView.state.doc.toString() + fileContent = fs.read(path) + if + fileContent is Absent then () + fileContent === currentContent then () + else + let + changes = mut { from: 0, to: tab.editorView.state.doc.length, insert: fileContent } + updateOptions = mut { :changes } + transaction = tab.editorView.state.update(updateOptions) + tab.editorView.dispatch(transaction) + + fun handleFsEventForTab(tabId, tab, event) = if + tab.path !== event.path then () + event.'type is + "delete" then this.closeTab(tabId) + "rename" then + set + tab.name = event.node.name + tab.path = event.newPath + this.updateDisplay() + "readonly" then + set tab.readonly = event.readonly + this.updateDisplay() + "attr" then + set tab.attrs = nodeAttrs(event) + this.updateDisplay() + "write" then updateTabContent(tab, event.path) + else () + + fun handleFsEvent(event) = if + event.'type is "reset" then this.resetTabsAfterFsReset() + shouldHandleFsEvent(event.'type) then + Array.from(this.openTabs.entries()).forEach of (entry, ...) => + this.handleFsEventForTab(entry.at(0), entry.at(1), event) + else () + + fun activeTab() = if this.activeTabId is + Absent then null + tabId then this.openTabs.get(tabId) + + fun tabPath(tab) = if tab is + Absent then null + activeTab then activeTab.path + + fun activeFilePath() = + tabPath(activeTab()) + + fun filePathDetail(path) = + mut { filePath: path } + + fun dispatchCompile() = + document.dispatchEvent(new CustomEvent("compile-requested", bubbleEventOptions(filePathDetail(activeFilePath())))) + + fun dispatchExecute() = if activeTab() is + Absent then () + activeTab then + document.dispatchEvent(new CustomEvent("execute-requested", bubbleEventOptions(filePathDetail(activeTab.path)))) + + fun isKey(event, lower, upper) = if + event.key === lower then true + event.key === upper then true + else false + + fun isFontSizeIncreaseKey(event) = + if + event.key === "=" then true + event.key === "+" then true + else false + + fun isFontSizeDecreaseKey(event) = + if + event.key === "-" then true + event.key === "_" then true + else false + + fun handleShortcut(event) = + let + isMac = window.navigator.platform.toUpperCase().indexOf("MAC") >= 0 + isCmdOrCtrl = if isMac then event.metaKey else event.ctrlKey + if isCmdOrCtrl do + if + isKey(event, "s", "S") then + event.preventDefault() + dispatchCompile() + isKey(event, "e", "E") then + event.preventDefault() + dispatchExecute() + isKey(event, "b", "B") and event.shiftKey then + event.preventDefault() + dispatchSidebarToggle("right", "problems") + isKey(event, "b", "B") then + event.preventDefault() + dispatchSidebarToggle("left", "files") + isFontSizeIncreaseKey(event) then + event.preventDefault() + editor.bumpEditorFontSize(1) + isFontSizeDecreaseKey(event) then + event.preventDefault() + editor.bumpEditorFontSize(-1) + else () + if event.ctrlKey and isKey(event, "w", "W") do + event.preventDefault() + if this.activeTabId is ~Absent as tabId do + this.closeTab(tabId) + + fun setupEventListeners() = + let panel = this + window.addEventListener of "file-open", event => + panel.openFile(event.detail.path, event.detail.fileName) + document.addEventListener("keydown", event => panel.handleShortcut(event)) + document.addEventListener("current-project-changed", () => panel.scheduleOpenTabsRestore()) + + fun showArrow(arrow) = if not arrow.classList.contains("visible") do + set arrow.style.display = "flex" + arrow.offsetHeight + arrow.classList.add("visible") + + fun hideArrow(arrow) = if arrow.classList.contains("visible") do + arrow.classList.remove("visible") + let hideLater = () => + if not arrow.classList.contains("visible") do + set arrow.style.display = "none" + window.setTimeout(hideLater, 200) + + fun setupTabBarScrolling() = + let + tabBar = this.querySelector(".tab-bar") + leftArrow = this.querySelector(".tab-scroll-left") + rightArrow = this.querySelector(".tab-scroll-right") + scrollInterval = null + scrollSpeed = 3 + tabBar.addEventListener of "wheel", event => + event.preventDefault() + set tabBar.scrollLeft += event.deltaY + let startScrollLeft = () => + if scrollInterval is Absent do + let scrollLeft = () => set tabBar.scrollLeft -= scrollSpeed + set scrollInterval = window.setInterval(scrollLeft, 10) + let startScrollRight = () => + if scrollInterval is Absent do + let scrollRight = () => set tabBar.scrollLeft += scrollSpeed + set scrollInterval = window.setInterval(scrollRight, 10) + let stopScroll = () => + if scrollInterval is ~Absent as interval do + window.clearInterval(interval) + set scrollInterval = null + leftArrow.addEventListener("mouseenter", startScrollLeft) + leftArrow.addEventListener("mouseleave", stopScroll) + rightArrow.addEventListener("mouseenter", startScrollRight) + rightArrow.addEventListener("mouseleave", stopScroll) + set this.updateArrowVisibility = () => + let + hasOverflow = tabBar.scrollWidth > tabBar.clientWidth + isAtStart = tabBar.scrollLeft === 0 + isAtEnd = tabBar.scrollLeft >= tabBar.scrollWidth - tabBar.clientWidth - 1 + if + hasOverflow and isAtStart then hideArrow(leftArrow) + hasOverflow then showArrow(leftArrow) + else hideArrow(leftArrow) + if + hasOverflow and isAtEnd then hideArrow(rightArrow) + hasOverflow then showArrow(rightArrow) + else hideArrow(rightArrow) + tabBar.addEventListener("scroll", this.updateArrowVisibility) + set this.resizeObserver = Reflect.construct(window.ResizeObserver, [this.updateArrowVisibility]) + this.resizeObserver.observe(tabBar) + window.setTimeout(this.updateArrowVisibility, 0) + + fun existingTabIdFor(filePath) = + let existingTabId = null + Array.from(this.openTabs.entries()).forEach of (entry, ...) => + if existingTabId is Absent and entry.at(1).path === filePath do + set existingTabId = entry.at(0) + existingTabId + + fun extensionFor(filePath) = if filePath.match(new RegExp("\\.(\\w+)$")) is + Absent then "" + matchResult then matchResult.at(1) + + fun attrsOrEmpty(nodeInfo) = if + nodeInfo is Absent then mut {} + nodeInfo.attrs is Absent then mut {} + else nodeInfo.attrs + + fun isReadonlyNode(nodeInfo) = if nodeInfo is + Absent then false + node then node.readonly is true + + fun tabRecord(fileName, filePath, editorDiv, editorView, isReadonly, attrs) = + mut + name: fileName + path: filePath + // BUG: `:field` puns currently miscompile in multiline `mut` records here. + editorDiv: editorDiv + editorView: editorView + readonly: isReadonly + attrs: attrs + + fun openFile(filePath, fileName) = + let existingTabId = existingTabIdFor(filePath) + if existingTabId is + Absent then + let tabId = "tab-" + this.tabCounter + set this.tabCounter += 1 + let editorDiv = document.createElement("div") + set editorDiv.className = "editor-codemirror" + let + content = fs.read(filePath) + initialContent = if content is Absent then "" else content + extension = extensionFor(filePath) + nodeInfo = fs.stat(filePath) + isReadonly = isReadonlyNode(nodeInfo) + attrs = attrsOrEmpty(nodeInfo) + editorView = editor.createEditor(editorDiv, initialContent, filePath, extension, isReadonly) + this.openTabs.set(tabId, tabRecord(fileName, filePath, editorDiv, editorView, isReadonly, attrs)) + if this.querySelector(".editor-container") is + ~null as editorContainer do editorContainer.appendChild(editorDiv) + this.switchTab(tabId) + this.updateDisplay() + this.emitOpenFilesChange() + tabId + tabId then + this.switchTab(tabId) + tabId + + fun openFileAtLine(filePath, line, column, length) = + let + fileName = filePath.split("/").pop() + tabId = this.openFile(filePath, fileName) + tab = this.openTabs.get(tabId) + if + tab is Absent then () + tab.editorView is Absent then () + else + let + linePos = tab.editorView.state.doc.line(line) + columnOffset = if typeof(column) is "number" and column > 0 then column else 0 + selectionStart = Math.min(linePos.to, linePos.from + columnOffset) + selectionEnd = if typeof(length) is "number" and length > 0 then Math.min(linePos.to, selectionStart + length) else selectionStart + selection = mut { anchor: selectionStart, head: selectionEnd } + options = mut { :selection, scrollIntoView: true } + tab.editorView.dispatch(options) + tab.editorView.focus() + + fun switchTab(tabId) = + Array.from(this.openTabs.values()).forEach of (tab, ...) => + tab.editorDiv.classList.remove("active") + let selectedTab = this.openTabs.get(tabId) + if selectedTab is ~Absent as tab do + tab.editorDiv.classList.add("active") + set this.activeTabId = tabId + tab.editorView.focus() + editor.dispatchCursorPosition(tab.editorView.state, tab.path) + this.updateDisplay() + this.scrollTabIntoView(tabId) + + fun closeTab(tabId) = + let tab = this.openTabs.get(tabId) + if tab is ~Absent as closingTab do + if closingTab.editorView is ~Absent as editorView do + editorView.destroy() + closingTab.editorDiv.remove() + this.openTabs.delete(tabId) + if this.activeTabId === tabId do + let remainingTabs = Array.from(this.openTabs.keys()) + if + remainingTabs.length > 0 then this.switchTab(remainingTabs.at(remainingTabs.length - 1)) + else + set this.activeTabId = null + this.notifyActiveTabChange(null) + this.updateDisplay() + this.emitOpenFilesChange() + + fun tabTargetPath(target) = if target is + Absent then null + tabTarget then tabTarget.dataset.path + + fun tabLogInfo(reason) = + mut + :reason + pendingTargetPath: tabTargetPath(this.tabTooltipPendingTarget) + hoverTargetPath: tabTargetPath(this.tabTooltipHoverTarget) + + fun clearTabTooltip(reason) = + let tooltip = this.querySelector("file-tooltip.tab-tooltip") + Logger.debug("Tab Tooltip", "Hide", tabLogInfo(reason)) + if this.tabTooltipTimeout is ~Absent as timeout do + window.clearTimeout(timeout) + set this.tabTooltipTimeout = null + set + this.tabTooltipPendingTarget = null + this.tabTooltipHoverTarget = null + if tooltip is ~Absent as tooltipElement do + tooltipElement.hide() + + fun baseNameHtml(tabName, lastDot) = if + lastDot > 0 then + let + baseName = tabName.substring(0, lastDot) + extension = tabName.substring(lastDot) + """""" + baseName + """""" + extension + """""" + else """""" + tabName + """""" + + fun tabHtml(tabId, tab) = + let + isActive = tabId === this.activeTabId + activeClass = if isActive then "active" else "" + lockHtml = if tab.readonly then """""" else "" + lastDot = tab.name.lastIndexOf(".") + nameHtml = baseNameHtml(tab.name, lastDot) + """ +
+ + """ + lockHtml + """ + """ + nameHtml + """ + + +
+ """ + + fun tabsHtml() = + Array.from(this.openTabs.entries()).map((entry, ...) => + tabHtml(entry.at(0), entry.at(1)) + ).join("") + + fun tabForElement(tabEl) = + this.openTabs.get(tabEl.getAttribute("data-tab-id")) + + fun tooltipInfo(tab) = + mut + path: tab.path + name: tab.name + size: tab.editorView.state.doc.length + placement: "bottom" + + fun tabHoverLog(tabEl, reason) = + mut + :reason + tabId: tabEl.getAttribute("data-tab-id") + path: tabEl.dataset.path + pendingTargetPath: tabTargetPath(this.tabTooltipPendingTarget) + hoverTargetPath: tabTargetPath(this.tabTooltipHoverTarget) + + fun tabShowLog(tabEl, reason, tooltip) = + let info = tabHoverLog(tabEl, reason) + set info.visible = tooltip.classList.contains("visible") + info + + fun showTooltip(tabEl, reason, tooltip) = if tabForElement(tabEl) is ~Absent as tab do + Logger.debug("Tab Tooltip", "Show", tabShowLog(tabEl, reason, tooltip)) + tooltip.show(tabEl, tooltipInfo(tab)) + + fun setupNameScroll(container) = if + container is Absent then () + container.dataset.scrollInit is ~Absent then () + else + let textEl = container.querySelector(".tab-name-text") + if textEl is ~Absent as textElement do + set container.dataset.scrollInit = "true" + let start = () => + let distance = textElement.scrollWidth - container.clientWidth + 16 + if distance > 0 do + let duration = Math.min(12, Math.max(4, distance / 40)) + textElement.style.setProperty("--scroll-distance", distance + "px") + textElement.style.setProperty("--scroll-duration", duration + "s") + textElement.classList.add("scrolling") + let stop = () => + textElement.classList.remove("scrolling") + textElement.style.removeProperty("--scroll-distance") + textElement.style.removeProperty("--scroll-duration") + container.addEventListener("mouseenter", start) + container.addEventListener("mouseleave", stop) + container.addEventListener("focus", start) + container.addEventListener("blur", stop) + + fun setupTabElement(tabEl, tooltip) = + let panel = this + setupNameScroll(tabEl.querySelector(".tab-name")) + tabEl.addEventListener of "mousedown", event => + if event.button === 1 do event.preventDefault() + tabEl.addEventListener of "auxclick", event => + if event.button === 1 do + event.preventDefault() + event.stopPropagation() + panel.closeTab(tabEl.getAttribute("data-tab-id")) + tabEl.addEventListener of "click", event => + if event.target.closest(".tab-close") is Absent do + panel.switchTab(tabEl.getAttribute("data-tab-id")) + tabEl.addEventListener of "mouseenter", () => + set panel.tabTooltipHoverTarget = tabEl + Logger.debug("Tab Tooltip", "Mouseenter", panel.tabHoverLog(tabEl, "mouseenter")) + tabEl.addEventListener of "mousemove", () => + if + panel.tabTooltipHoverTarget !== tabEl then () + tooltip is Absent then () + tooltip.classList.contains("visible") then showTooltip(tabEl, "already-visible", tooltip) + panel.tabTooltipPendingTarget === tabEl then () + else + if panel.tabTooltipTimeout is ~Absent as timeout do + window.clearTimeout(timeout) + set panel.tabTooltipPendingTarget = tabEl + Logger.debug("Tab Tooltip", "Schedule show", panel.tabHoverLog(tabEl, "schedule")) + let showLater = () => + if panel.tabTooltipHoverTarget === tabEl do + showTooltip(tabEl, "delay-elapsed", tooltip) + set + panel.tabTooltipPendingTarget = null + panel.tabTooltipTimeout = null + set panel.tabTooltipTimeout = window.setTimeout(showLater, 5000) + tabEl.addEventListener("mouseleave", () => panel.clearTabTooltip("mouseleave")) + + fun setupCloseButton(btn) = + let panel = this + btn.addEventListener of "click", event => + event.stopPropagation() + panel.closeTab(btn.getAttribute("data-tab-id")) + + fun updateDisplay() = + let + tabBar = this.querySelector(".tab-bar") + emptyState = this.querySelector(".empty-state") + tooltip = this.querySelector("file-tooltip.tab-tooltip") + this.clearTabTooltip("rebuild-tab-bar") + set tabBar.innerHTML = tabsHtml() + if + this.openTabs.size === 0 then emptyState.classList.remove("hidden") + else emptyState.classList.add("hidden") + this.notifyActiveTabChange(activeTab()) + this.emitOpenFilesChange() + this.persistOpenTabs() + tabBar.querySelectorAll(".tab").forEach of (tabEl, ...) => + setupTabElement(tabEl, tooltip) + tabBar.querySelectorAll(".tab-close").forEach of (btn, ...) => + setupCloseButton(btn) + tabBar.addEventListener("scroll", () => this.clearTabTooltip("tab-bar-scroll")) + if this.updateArrowVisibility is ~Absent as updateArrowVisibility do + window.setTimeout(updateArrowVisibility, 0) + + fun scrollOptions(left) = + mut { :left, behavior: "smooth" } + + fun scrollTabIntoView(tabId) = + let scrollLater = () => + let + tabBar = this.querySelector(".tab-bar") + tabElement = this.querySelector(".tab[data-tab-id=\"" + tabId + "\"]") + if + tabBar is Absent then () + tabElement is Absent then () + else + let + tabBarRect = tabBar.getBoundingClientRect() + tabRect = tabElement.getBoundingClientRect() + isVisible = tabRect.left >= tabBarRect.left and tabRect.right <= tabBarRect.right + if not isVisible do + let scrollOffset = tabElement.offsetLeft - tabBar.offsetLeft - 20 + tabBar.scrollTo(scrollOptions(scrollOffset)) + window.setTimeout(scrollLater, 0) + + fun isStdTab(tab) = if + tab is Absent then false + tab.attrs is Absent then false + tab.attrs.std is true then true + else false + + fun activeTabDetail(tab) = + mut { path: tabPath(tab), isStd: isStdTab(tab) } + + fun notifyActiveTabChange(tab) = + document.dispatchEvent(new CustomEvent("active-tab-changed", eventOptions(activeTabDetail(tab)))) + +customElements.define("editor-panel", EditorPanel) diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/components/EditorWorkbench.mls b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/EditorWorkbench.mls new file mode 100644 index 0000000000..d895bf3fda --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/EditorWorkbench.mls @@ -0,0 +1,239 @@ +import "../common/JS.mls" + +open JS { Absent } + +fun escapeHtml(text) = + let div = document.createElement("div") + set div.textContent = text + div.innerHTML + +class EditorWorkbench extends HTMLElement with + let + activeFilePath = null + splitOpen = false + target = "mjs" + feedbackText = "" + + fun connectedCallback() = + this.render() + this.attachEventListeners() + this.updateBreadcrumbs() + this.updateCompiledPane() + + fun render() = + set this.innerHTML = """ +
+
+ + +
+
+ + +
+
+ """ + + fun targetOptionHtml(value, label) = + """ + + """ + + fun setActiveFilePath(path) = + set activeFilePath = path + updateBreadcrumbs() + updateCompiledPane() + + fun handleActiveTabChanged(detail) = + let path = if + detail is Absent then null + detail.path is Absent then null + else detail.path + setActiveFilePath(path) + + fun activeFileLabel() = if activeFilePath is + null then "No file" + path then path + + fun breadcrumbSegmentHtml(label, active) = + let className = if active then "editor-breadcrumb-segment editor-breadcrumb-active" else "editor-breadcrumb-segment" + """""" + escapeHtml(label) + """""" + + fun breadcrumbHtml() = + let html = """workspace""" + if activeFilePath is + null then html + """""" + breadcrumbSegmentHtml("No file", true) + path then + let parts = path.split("/").filter((part, ...) => part !== "") + if parts.length === 0 then html + else + parts.forEach of (part, index) => + set html += """""" + breadcrumbSegmentHtml(part, index === parts.length - 1) + html + + fun activeFileName() = if activeFilePath is + null then "untitled" + path then path.split("/").pop() + + fun updateBreadcrumbs() = + if this.querySelector(".editor-breadcrumbs") is ~null as breadcrumbs do + set breadcrumbs.innerHTML = breadcrumbHtml() + + fun openCompiledOutput() = + set + splitOpen = true + feedbackText = "" + this.classList.add("compiled-open") + if this.querySelector(".compiled-output-pane") is ~null as pane do + set pane.hidden = false + updateCompiledPane() + + fun closeCompiledOutput() = + set + splitOpen = false + feedbackText = "" + this.classList.remove("compiled-open") + if this.querySelector(".compiled-output-pane") is ~null as pane do + set pane.hidden = true + + fun setTarget(nextTarget) = if nextTarget is + "mjs" | "wasm" | "c" then + set + target = nextTarget + feedbackText = "" + updateCompiledPane() + else () + + fun targetLabel(value) = if value is + "wasm" then "wasm" + "c" then "c" + else "mjs" + + fun currentOutput() = if target is + "wasm" then wasmOutput() + "c" then cOutput() + else mjsOutput() + + fun mjsOutput() = + "import { print } from \"./mlscript-std/Predef.mjs\";\n\n" + + "print(\"Generated preview for " + activeFileName() + "\");\n" + + fun wasmOutput() = + "(module\n" + + " (import \"env\" \"print\" (func $print (param i32)))\n" + + " (func $main\n" + + " ;; mock wasm output for " + activeFileName() + "\n" + + " )\n" + + ")\n" + + fun cOutput() = + "#include \n\n" + + "int main(void) {\n" + + " puts(\"Generated preview for " + activeFileName() + "\");\n" + + " return 0;\n" + + "}\n" + + fun updateCompiledPane() = + this.querySelectorAll(".compiled-target-option input").forEach of (input, ...) => + set input.checked = input.value === target + if this.querySelector(".compiled-output-code") is ~null as output do + set output.textContent = currentOutput() + if this.querySelector(".compiled-output-file") is ~null as file do + set file.textContent = activeFileLabel() + " Β· " + targetLabel(target) + if this.querySelector(".compiled-output-feedback") is ~null as feedback do + if feedbackText === "" then + set feedback.hidden = true + else + set + feedback.hidden = false + feedback.textContent = feedbackText + + fun copyCurrentOutput() = + let textArea = document.createElement("textarea") + set + textArea.value = currentOutput() + textArea.style.position = "fixed" + textArea.style.left = "-1000px" + textArea.style.top = "-1000px" + document.body.appendChild(textArea) + textArea.focus() + textArea.select() + let copied = document.execCommand("copy") + textArea.remove() + set feedbackText = if copied then "Copied " + targetLabel(target) else "Copy failed" + updateCompiledPane() + + fun downloadCurrentOutput() = + let + blob = new! window.Blob([currentOutput()], mut { 'type: "text/plain" }) + url = window.URL.createObjectURL(blob) + link = document.createElement("a") + set + link.href = url + link.download = "mlscript-output." + targetLabel(target) + link.style.display = "none" + document.body.appendChild(link) + link.click() + link.remove() + window.URL.revokeObjectURL(url) + set feedbackText = "Downloaded " + targetLabel(target) + updateCompiledPane() + + fun attachEventListeners() = + let shell = this + if this.querySelector(".open-compiled-output") is ~null as openButton do + openButton.addEventListener("click", () => shell.openCompiledOutput()) + if this.querySelector(".compiled-close-button") is ~null as closeButton do + closeButton.addEventListener("click", () => shell.closeCompiledOutput()) + if this.querySelector(".compiled-copy-button") is ~null as copyButton do + copyButton.addEventListener("click", () => shell.copyCurrentOutput()) + if this.querySelector(".compiled-download-button") is ~null as downloadButton do + downloadButton.addEventListener("click", () => shell.downloadCurrentOutput()) + this.querySelectorAll(".compiled-target-option input").forEach of (input, ...) => + input.addEventListener("change", () => shell.setTarget(input.value)) + document.addEventListener("active-tab-changed", event => shell.handleActiveTabChanged(event.detail)) + +customElements.define("editor-workbench", EditorWorkbench) diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/components/FileExplorer.mls b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/FileExplorer.mls new file mode 100644 index 0000000000..e979c4bedd --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/FileExplorer.mls @@ -0,0 +1,316 @@ +import "../filesystem/fs.mls" +import "./PanelPersistence.mls" +import "./TreeNode.mls" +import "../common/JS.mls" + +open JS { Absent } + +fun basenameWithoutExt(name) = + name.slice(0, -4) + +fun forceOptions() = + mut { force: true } + +class FileExplorer extends HTMLElement with + let + isCollapsed = false + rootNodes = new Map() + isCreatingFile = false + newFileInput = null + tooltipTimeout = null + activeTooltipTarget = null + openFiles = new Set() + unsubscribe = null + openFilesChangedListener = null + newFileRequestedListener = null + + fun connectedCallback() = + this.render() + this.attachEventListeners() + this.attachTooltipHandlers() + this.restoreSizeFromStorage() + let explorer = this + set openFilesChangedListener = event => explorer.handleOpenFilesChanged(event) + set newFileRequestedListener = () => explorer.startCreatingFile() + document.addEventListener("open-files-changed", openFilesChangedListener) + document.addEventListener("new-file-requested", newFileRequestedListener) + set unsubscribe = fs.subscribe(event => explorer.handleFsEvent(event), undefined) + + fun restoreSizeFromStorage() = + PanelPersistence.restorePanelWidth(this, "file-explorer-width", 150, 600) + + fun saveSizeToStorage(width) = + PanelPersistence.savePanelWidth("file-explorer-width", width) + + fun disconnectedCallback() = + if unsubscribe is ~Absent as removeSubscription do removeSubscription() + if tooltipTimeout is ~Absent as timeout do + window.clearTimeout(timeout) + set tooltipTimeout = null + if openFilesChangedListener is ~Absent as listener do + document.removeEventListener("open-files-changed", listener) + if newFileRequestedListener is ~Absent as listener do + document.removeEventListener("new-file-requested", listener) + + fun render() = + set this.innerHTML = """ +
+

Files

+ +
+
+ + """ + this.updateTree() + + fun handleOpenFilesChanged(event) = + let paths = if + event.detail is Absent then [] + event.detail.paths is Absent then [] + else event.detail.paths + set openFiles = new Set(paths) + this.updateOpenFlags() + + fun resetRootNodes() = + Array.from(rootNodes.values()).forEach of (element, ...) => element.remove() + rootNodes.clear() + + fun handleFsEvent(event) = if event.'type is + "reset" then + resetRootNodes() + this.updateTree() + "create" | "delete" | "rename" then updateTreeForRootEvent(event) + else () + + fun updateTreeForRootEvent(event) = + let pathParts = event.path.replace(new RegExp("^/"), "").split("/") + if pathParts.length is 1 do this.updateTree() + + fun filterMjsFiles(children) = + let mlsFiles = new Set() + children.forEach of (child, ...) => + if child.'type is "file" and child.name.endsWith(".mls") do + mlsFiles.add(basenameWithoutExt(child.name)) + children.filter of (child, ...) => + if + child.'type is "file" and child.name.endsWith(".mjs") then + let basename = basenameWithoutExt(child.name) + not mlsFiles.has(basename) + else true + + fun rootPath(node) = + "/" + node.name + + fun updateTree() = if this.querySelector(".tree-view") is + ~null as treeView do + let + filteredRootNodes = filterMjsFiles(fs.fileTree) + newRootPaths = new Set() + filteredRootNodes.forEach of (node, ...) => + newRootPaths.add(rootPath(node)) + Array.from(rootNodes.entries()).forEach of (entry, ...) => + let + existingPath = entry.at(0) + element = entry.at(1) + if not newRootPaths.has(existingPath) do + element.remove() + rootNodes.delete(existingPath) + filteredRootNodes.forEach of (node, index) => + let path = rootPath(node) + if + not rootNodes.has(path) then + let treeNode = document.createElement("tree-node") + treeNode.setData(node, path, fs.fileTree) + rootNodes.set(path, treeNode) + let nextChild = treeView.children.(index) + if nextChild is + ~Absent as child then treeView.insertBefore(treeNode, child) + Absent then treeView.appendChild(treeNode) + else + let treeNode = rootNodes.get(path) + treeNode.setData(node, path, fs.fileTree) + let currentPosition = Array.from(treeView.children).indexOf(treeNode) + if currentPosition !== index do + let nextChild = treeView.children.(index) + if nextChild !== treeNode do treeView.insertBefore(treeNode, nextChild) + this.updateOpenFlags() + + fun toggleCollapse() = + set isCollapsed = not isCollapsed + this.classList.toggle("collapsed", isCollapsed) + syncWidthForCollapseState(document.querySelector(".app-container")) + + fun syncWidthForCollapseState(container) = if + container is Absent then () + isCollapsed then container.style.removeProperty("--file-explorer-width") + else restoreWidthIfExpanded(container) + + fun restoreWidthIfExpanded(container) = if + isCollapsed then () + container is Absent then () + else + let width = this.getAttribute("width") + if width is + Absent | "" then container.style.removeProperty("--file-explorer-width") + savedWidth then container.style.setProperty("--file-explorer-width", savedWidth + "px") + + fun focusNewFileInput(input) = + if input is ~Absent do + window.setTimeout( + () => + input.focus() + input.select(), + 0 + ) + + fun startCreatingFile() = if not isCreatingFile and this.querySelector(".tree-view") is + ~null as treeView do + let explorer = this + set isCreatingFile = true + let + fileEntry = document.createElement("div") + fileIcon = document.createElement("i") + input = document.createElement("input") + set + fileEntry.className = "file-item new-file-entry" + fileIcon.className = "file-icon icon-file-code" + input.'type = "text" + input.className = "new-file-input" + input.placeholder = "path/to/file.mls" + fileEntry.appendChild(fileIcon) + fileEntry.appendChild(input) + treeView.insertBefore(fileEntry, treeView.firstChild) + set newFileInput = input + focusNewFileInput(input) + input.addEventListener("keydown", event => explorer.handleNewFileKeydown(event, input)) + input.addEventListener of "blur", () => + window.setTimeout(() => explorer.cancelIfCreatingFile(), 200) + else if isCreatingFile do + focusNewFileInput(newFileInput) + + fun cancelIfCreatingFile() = if isCreatingFile do this.cancelCreatingFile() + + fun handleNewFileKeydown(event, input) = if event.key is + "Enter" then + event.preventDefault() + submitNewFile(input) + "Escape" then + event.preventDefault() + cancelCreatingFile() + else () + + fun normalizeInputPath(fileName) = + fileName + .replaceAll("\\", "/") + .replace(new RegExp("^/+"), "") + .replace(new RegExp("/+$"), "") + + fun invalidPathPart(part) = + part is ("." | "..") + + fun submitNewFile(input) = + let fileName = input.value.trim() + if + fileName is "" then cancelCreatingFile() + else + let + normalizedInput = normalizeInputPath(fileName) + parts = normalizedInput.split("/").filter of (part, ...) => part !== "" + hasInvalidPart = parts.some of (part, ...) => invalidPathPart(part) + if + parts.length is 0 then + window.alert("Please enter a valid file name (e.g. path/to/file.mls)") + input.focus() + hasInvalidPart then + window.alert("File name cannot contain \".\" or \"..\" path segments") + input.focus() + else + let + path = "/" + parts.join("/") + success = fs.createFile(path, "", forceOptions()) + if + success then + cancelCreatingFile() + dispatchFileOpen(path, fileName) + else + window.alert("Failed to create file. File may already exist.") + input.focus() + + fun cancelCreatingFile() = if isCreatingFile do + set isCreatingFile = false + if this.querySelector(".new-file-entry") is + ~null as entry do entry.remove() + set newFileInput = null + + fun fileOpenDetail(path, fileName) = + mut { :path, :fileName } + + fun fileOpenOptions(path, fileName) = + mut { detail: fileOpenDetail(path, fileName), bubbles: true } + + fun dispatchFileOpen(path, fileName) = + this.dispatchEvent(new CustomEvent("file-open", fileOpenOptions(path, fileName))) + + fun attachEventListeners() = + let explorer = this + if this.querySelector(".create-file-button") is + ~null as createFileBtn do createFileBtn.addEventListener("click", () => explorer.startCreatingFile()) + + fun attachTooltipHandlers() = + let + treeView = this.querySelector(".tree-view") + tooltip = this.querySelector("file-tooltip.tree-tooltip") + if + treeView is Absent then () + tooltip is Absent then () + else + let explorer = this + treeView.addEventListener("mouseover", event => explorer.handleTreeMouseOver(event, treeView, tooltip)) + treeView.addEventListener("mouseout", event => explorer.handleTreeMouseOut(event, tooltip)) + treeView.addEventListener("scroll", () => explorer.hideTooltip(tooltip)) + this.addEventListener("mouseleave", () => explorer.hideTooltip(tooltip)) + + fun hideTooltip(tooltip) = + if tooltipTimeout is ~Absent as timeout do + window.clearTimeout(timeout) + set tooltipTimeout = null + tooltip.hide() + set activeTooltipTarget = null + + fun handleTreeMouseOver(event, treeView, tooltip) = + let target = event.target.closest(".file-item, summary") + if target is ~Absent as targetElement and treeView.contains(targetElement) do + if activeTooltipTarget !== targetElement do + hideTooltip(tooltip) + set activeTooltipTarget = targetElement + let currentTarget = targetElement + set tooltipTimeout = window.setTimeout of + () => this.showTooltipIfActive(currentTarget, tooltip) + 5000 + + fun tooltipName(target) = if target.dataset.name is + Absent | "" then target.textContent.trim() + name then name + + fun tooltipOptions(targetPath, targetName) = + mut { path: targetPath, name: targetName } + + fun showTooltipIfActive(currentTarget, tooltip) = + if currentTarget.dataset.path is ~Absent as targetPath and targetPath !== "" and activeTooltipTarget === currentTarget do + tooltip.show(currentTarget, tooltipOptions(targetPath, tooltipName(currentTarget))) + + fun handleTreeMouseOut(event, tooltip) = + if activeTooltipTarget is ~Absent as tooltipTarget do + let related = event.relatedTarget + if related is + ~Absent as nextTarget and tooltipTarget.contains(nextTarget) then () + ~Absent as nextTarget and nextTarget.closest(".file-item, summary") === tooltipTarget then () + else hideTooltip(tooltip) + + fun updateOpenFlags() = + Array.from(rootNodes.values()).forEach of (treeNode, ...) => + treeNode.updateOpenState(openFiles) + +customElements.define("file-explorer", FileExplorer) diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/components/FileTooltip.mls b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/FileTooltip.mls new file mode 100644 index 0000000000..a6f645c59f --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/FileTooltip.mls @@ -0,0 +1,203 @@ +import "../filesystem/fs.mls" +import "../common/JS.mls" +import "../common/Logger.mls" +import "https://esm.sh/@floating-ui/dom" { computePosition, shift, offset } + +open JS { Absent } + +module fileTooltip with... + +fun textOf(value) = + String(value) + +fun escapeChar(ch) = if ch is + "&" then "&" + "<" then "<" + ">" then ">" + "\"" then """ + "'" then "'" + else ch + +fun escapeHtml(value) = + let + text = textOf(value) + result = "" + i = 0 + while i < text.length do + set result += escapeChar(text.charAt(i)) + set i += 1 + result + +fun attrFallback(value) = if + value is Absent then "-" + typeof(value) is "object" then JSON.stringify(value) + value is true then "true" + value is false then "false" + else textOf(value) + +fun relativeTimeOptions() = + mut { numeric: "auto" } + +fun makeRelativeTimeFormatter() = if + Intl is Absent then null + Intl.RelativeTimeFormat is Absent then null + else Reflect.construct(Intl.RelativeTimeFormat, [undefined, relativeTimeOptions()]) + +fun dateFrom(value) = + new Date(value) + +class FileTooltip extends HTMLElement with + let + relativeTimeFormatter = makeRelativeTimeFormatter() + showRequestId = 0 + + fun connectedCallback() = + this.classList.add("file-tooltip") + this.setAttribute("role", "tooltip") + + fun formatRelativeTime(timestamp) = if + relativeTimeFormatter is Absent then "" + not Number.isFinite(timestamp) then "" + else + let divisions = [ + (amount: 60, unit: "seconds") + (amount: 60, unit: "minutes") + (amount: 24, unit: "hours") + (amount: 7, unit: "days") + (amount: 4.34524, unit: "weeks") + (amount: 12, unit: "months") + (amount: Number.POSITIVE_INFINITY, unit: "years") + ] + let + duration = (timestamp - Date.now()) / 1000 + result = "" + i = 0 + while result === "" and i < divisions.length do + let division = divisions.at(i) + if Math.abs(duration) < division.amount then + set result = relativeTimeFormatter.format( + Math.round(duration) + division.unit.slice(0, -1) + ) + else + set duration /= division.amount + set i += 1 + result + + fun formatTimestamp(value) = if not Number.isFinite(value) then "-" + else + let absolute = dateFrom(value).toLocaleString(undefined, + year: "numeric" + month: "short" + day: "numeric" + hour: "2-digit" + minute: "2-digit" + ) + let relative = this.formatRelativeTime(value) + if + relative !== "" then absolute + " - " + relative + else absolute + + fun formatSize(size) = if + not Number.isFinite(size) then "-" + size < 0 then "-" + size === 1 then "1 byte" + size < 1024 then textOf(size) + " bytes" + let kb = size / 1024 + kb < 1024 then kb.toFixed(1) + " KB" + else (kb / 1024).toFixed(1) + " MB" + + fun attrText(attrs) = if + attrs is Absent then "-" + Object.keys(attrs).length === 0 then "-" + else Object.entries(attrs) + .map((entry, ...) => + entry.at(0) + ": " + escapeHtml(attrFallback(entry.at(1))) + ) + .join(", ") + + fun displayName(path, node, name) = if + name is ~Absent and name !== "" then name + node.name is ~Absent and node.name !== "" then node.name + else path.split("/").pop() + + fun fileSize(node, sizeOverride) = if + sizeOverride is ~Absent then sizeOverride + typeof(node.content) is "string" then node.content.length + else undefined + + fun tooltipContent(path, name, sizeOverride) = if + path is Absent then null + path !== "" then + let node = fs.stat(path) + if node is Absent then null + else + let + size = fileSize(node, sizeOverride) + attrs = attrText(node.attrs) + shownName = displayName(path, node, name) + """ +
+
Name
+
""" + escapeHtml(shownName) + """
+
Path
+
""" + escapeHtml(path) + """
+
Size
+
""" + this.formatSize(size) + """
+
Modified
+
""" + this.formatTimestamp(node.mtime) + """
+
Created
+
""" + this.formatTimestamp(node.birthtime) + """
+
Readonly
+
""" + (if node.readonly then "Yes" else "No") + """
+
Attributes
+
""" + attrs + """
+
+ """ + else null + + fun show(targetEl, optionsArg) = + let + options = if optionsArg is Absent then new Object else optionsArg + path = options.path + name = options.name + placement = if + options.placement is Absent then "right" + else options.placement + content = this.tooltipContent(path, name, options.size) + if content is ~Absent and targetEl is ~Absent do + Logger.debug("File Tooltip", "Request show", + path: path + name: name + placement: placement + targetPath: if targetEl.dataset is Absent then undefined else targetEl.dataset.path + targetName: if targetEl.dataset is Absent then undefined else targetEl.dataset.name + ) + set this.innerHTML = content + set this.dataset.placement = placement + set showRequestId += 1 + let requestId = showRequestId + computePosition(targetEl, this, + placement: placement + strategy: "fixed" + middleware: [shift(padding: 5), offset(8)] + ).then of position => + if requestId === showRequestId do + set + this.style.top = position.y + "px" + this.style.left = position.x + "px" + this.classList.add("visible") + Logger.debug("File Tooltip", "Shown", + path: path + placement: placement + x: position.x + y: position.y + ) + + fun hide() = + set showRequestId += 1 + this.classList.remove("visible") + Reflect.deleteProperty(this.dataset, "placement") + Logger.debug("File Tooltip", "Hide") + +customElements.define("file-tooltip", FileTooltip) diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/components/IdeWorkbench.mls b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/IdeWorkbench.mls new file mode 100644 index 0000000000..a66a040f83 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/IdeWorkbench.mls @@ -0,0 +1,248 @@ +import "../common/JS.mls" +import "../common/SettingsStore.mls" +import "./ResizeHandle.mls" + +open JS { Absent } + +class IdeWorkbench extends HTMLElement with + let + leftActivePanel = "files" + leftPanelOpen = true + problemsOpen = true + viewportResizeListener = null + + fun connectedCallback() = + this.restoreStartupSettings() + this.render() + this.attachEventListeners() + this.syncLayoutState() + this.attachViewportListener() + this.applyNarrowLayout() + + fun disconnectedCallback() = + if viewportResizeListener is ~Absent as listener do + window.removeEventListener("resize", listener) + + fun restoreStartupSettings() = + set problemsOpen = SettingsStore.getSetting("workbench.showProblemsOnStartup", "true") !== "false" + + fun render() = + set this.innerHTML = """ +
+ +
+ + + + + + + + + + + +
+ +
+ Ready + + + + UTF-8 LF + Spaces: 2 + No file + No language +
+
+ + + + + """ + + fun appContainer() = + this.querySelector(".app-container") + + fun activePanelAttribute(side) = + "data-" + side + "-active-panel" + + fun sidebarPanelSelector(side) = + ".sidebar-panel[data-sidebar-side=\"" + side + "\"][data-sidebar-panel]" + + fun sidebarButtonSelector(side) = + ".activity-button[data-sidebar-side=\"" + side + "\"]" + + fun panelIsActive(panel, panelName, isOpen) = + if isOpen then panel.dataset.sidebarPanel === panelName else false + + fun setSidebarButtons(side, panelName, isOpen) = + this.querySelectorAll(sidebarButtonSelector(side)).forEach of (button, ...) => + let isActive = panelIsActive(button, panelName, isOpen) + button.classList.toggle("active", isActive) + button.setAttribute("aria-pressed", if isActive then "true" else "false") + + fun setSidebarPanels(side, panelName, isOpen) = + this.querySelectorAll(sidebarPanelSelector(side)).forEach of (panel, ...) => + let isActive = panelIsActive(panel, panelName, isOpen) + panel.classList.toggle("active", isActive) + set panel.hidden = not isActive + + fun setLeftPanelState(panelName, isOpen) = + set + leftActivePanel = panelName + leftPanelOpen = isOpen + if appContainer() is ~null as container do + container.setAttribute(activePanelAttribute("left"), panelName) + container.classList.toggle("left-sidebar-closed", not isOpen) + setSidebarPanels("left", panelName, isOpen) + setSidebarButtons("left", panelName, isOpen) + + fun toggleLeftPanel(panelName) = + if + leftActivePanel === panelName and leftPanelOpen then setLeftPanelState(panelName, false) + else setLeftPanelState(panelName, true) + + fun setProblemsState(isOpen) = + set problemsOpen = isOpen + if appContainer() is ~null as container do + container.classList.toggle("right-sidebar-closed", not isOpen) + setSidebarPanels("right", "problems", isOpen) + setSidebarButtons("right", "problems", isOpen) + + fun toggleProblems() = + setProblemsState(not problemsOpen) + + fun fileKind(path) = + if + path is Absent then "No language" + path.endsWith(".mls") then "MLscript" + path.endsWith(".mjs") then "JavaScript" + path.endsWith(".md") then "Markdown" + path.endsWith(".markdown") then "Markdown" + else "File" + + fun updateText(selector, value) = + if this.querySelector(selector) is ~null as element do + set element.textContent = value + + fun setHidden(selector, isHidden) = + if this.querySelector(selector) is ~null as element do + set element.hidden = isHidden + + fun finiteNumber(value) = + typeof(value) is "number" and value === value + + fun statusNumber(value, fallback) = + if finiteNumber(value) then value else fallback + + fun hideEditorPositionStatus() = + setHidden(".cursor-position-status", true) + setHidden(".selection-status", true) + + fun updateActiveFileStatus(detail) = + let path = if + detail is Absent then "No file" + detail.path is Absent then "No file" + else detail.path + updateText(".active-file-status", path) + updateText(".active-file-kind", fileKind(path)) + if path === "No file" do hideEditorPositionStatus() + + fun updateCursorPositionStatus(detail) = + if detail is ~Absent as position do + let + line = statusNumber(position.line, 1) + column = statusNumber(position.column, 1) + selected = statusNumber(position.selected, 0) + updateText(".cursor-position-status", "Ln " + line + ", Col " + column) + setHidden(".cursor-position-status", false) + if selected > 0 then + updateText(".selection-status", "(" + selected + " selected)") + setHidden(".selection-status", false) + else setHidden(".selection-status", true) + + fun updateWorkbenchStatus(status) = + if status is + "running" then updateText(".workbench-state-status", "Running") + "done" then updateText(".workbench-state-status", "Ready") + "error" then updateText(".workbench-state-status", "Error") + "aborted" then updateText(".workbench-state-status", "Aborted") + "fatal" then updateText(".workbench-state-status", "Fatal error") + else updateText(".workbench-state-status", "Ready") + + fun updateCompilationStatus(status) = + if status is + "running" then updateText(".workbench-state-status", "Compiling") + "fatal" then updateText(".workbench-state-status", "Fatal error") + "error" then updateText(".workbench-state-status", "Error") + else updateText(".workbench-state-status", "Ready") + + fun handleSidebarToggleRequested(event) = + if event.detail is ~Absent as detail do + let + side = if detail.side is ~Absent then detail.side else "left" + panelName = if detail.panel is ~Absent then detail.panel else "files" + if side is + "right" then + if detail.forceOpen === true then setProblemsState(true) + else toggleProblems() + else + if detail.forceOpen === true then setLeftPanelState(panelName, true) + else toggleLeftPanel(panelName) + + fun syncLayoutState() = + setLeftPanelState(leftActivePanel, leftPanelOpen) + setProblemsState(problemsOpen) + + fun isNarrowLayout() = + window.matchMedia("(max-width: 760px)").matches + + fun applyNarrowLayout() = + if isNarrowLayout() do + if leftPanelOpen do setLeftPanelState(leftActivePanel, false) + if problemsOpen do setProblemsState(false) + + fun attachViewportListener() = + let workbench = this + set viewportResizeListener = () => workbench.applyNarrowLayout() + window.addEventListener("resize", viewportResizeListener) + + fun attachActivityButtons() = + this.querySelectorAll(".activity-button[data-sidebar-side]").forEach of (button, ...) => + button.addEventListener of "click", () => + if button.dataset.sidebarSide is + "right" then toggleProblems() + else toggleLeftPanel(button.dataset.sidebarPanel) + + fun attachEventListeners() = + attachActivityButtons() + document.addEventListener("sidebar-toggle-requested", event => handleSidebarToggleRequested(event)) + document.addEventListener("active-tab-changed", event => updateActiveFileStatus(event.detail)) + document.addEventListener("cursor-position-changed", event => updateCursorPositionStatus(event.detail)) + document.addEventListener("compilation-status-change", event => updateCompilationStatus(event.detail.status)) + window.addEventListener("execution-status-change", event => updateWorkbenchStatus(event.detail.status)) + +customElements.define("ide-workbench", IdeWorkbench) diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/components/MockActivityPanels.mls b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/MockActivityPanels.mls new file mode 100644 index 0000000000..5cbe48cfbe --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/MockActivityPanels.mls @@ -0,0 +1,165 @@ +import "../mockWorkbenchData.mls" +import "../filesystem/fs.mls" +import "../common/JS.mls" + +open JS { Absent } + +fun escapeHtml(text) = + let div = document.createElement("div") + set div.textContent = text + div.innerHTML + +fun panelHeader(title, detail) = + let detailHtml = if detail === "" then "" else """""" + detail + """""" + """ +
+

""" + title + """

+ """ + detailHtml + """ +
+ """ + +val examples = [ + mut {id: "pattern-matching", title: "Pattern Matching", code: "data class Cons(head, tail)\ndata object Nil\n\nfun sum(list) = if list is\n Cons(h, t) then h + sum(t)\n Nil then 0\n\nsum(Cons(1, Cons(2, Cons(3, Nil))))"} + mut {id: "recursion", title: "Recursion", code: "fun factorial(n) =\n if n <= 1 then 1\n else n * factorial(n - 1)\n\nfactorial(5)"} +] + +class SourceControlPanel extends HTMLElement with + let stagedPaths = new Set() + + fun connectedCallback() = + this.render() + + fun isStaged(change) = + stagedPaths.has(change.path) + + fun sectionChanges(inStageSection) = + mockWorkbenchData.sourceChanges.filter(change => isStaged(change) === inStageSection) + + fun changeHtml(change, inStageSection) = + let + action = if inStageSection then "Unstage" else "Stage" + icon = if inStageSection then "icon-minus" else "icon-plus" + """ +
+
+ """ + escapeHtml(change.path) + """ + """ + escapeHtml(change.summary) + """ +
+ """ + escapeHtml(change.status) + """ + +
+ """ + + fun sectionContent(changes, inStageSection) = + if changes.length is + 0 then """
Nothing here
""" + else changes.map(change => changeHtml(change, inStageSection)).join("") + + fun sectionHtml(title, inStageSection) = + let changes = sectionChanges(inStageSection) + """ +
+
+ """ + title + """ + """ + changes.length + """ +
+ """ + sectionContent(changes, inStageSection) + """ +
+ """ + + fun render() = + set this.innerHTML = """ +
+ """ + panelHeader("Source Control", "Mock changes") + """ +
+ """ + sectionHtml("Staged", true) + """ + """ + sectionHtml("Changes", false) + """ +
+
+ """ + attachStageButtons() + + fun toggleStage(path) = + if stagedPaths.has(path) then stagedPaths.delete(path) + else stagedPaths.add(path) + render() + + fun attachStageButtons() = + let panel = this + this.querySelectorAll(".mock-stage-button").forEach of (button, ...) => + button.addEventListener("click", () => panel.toggleStage(button.dataset.filePath)) + +class ExamplesPanel extends HTMLElement with + let selectedId = "pattern-matching" + + fun connectedCallback() = + this.render() + + fun selectedExample() = + let match = examples.find(example => example.id === selectedId) + if match is + Absent then examples.at(0) + example then example + + fun exampleButtonHtml(example) = + let activeClass = if example.id === selectedId then " active" else "" + """ + + """ + + fun detailHtml(example) = + """ +
+
""" + escapeHtml(example.title) + """
+
""" + escapeHtml(example.code) + """
+ +
+ """ + + fun render() = + let example = selectedExample() + set this.innerHTML = """ +
+ """ + panelHeader("Examples", "") + """ +
+
""" + examples.map(item => exampleButtonHtml(item)).join("") + """
+ """ + detailHtml(example) + """ +
+
+ """ + attachExampleRows() + + fun selectExample(id) = + set selectedId = id + render() + + fun forceOptions() = + mut { force: true } + + fun examplePath(example) = + "/examples/" + example.id + ".mls" + + fun fileOpenOptions(path, fileName) = + mut { detail: mut { :path, :fileName }, bubbles: true } + + fun loadExample(id) = + let example = examples.find(item => item.id === id) + if example is ~Absent as selected do + let path = examplePath(selected) + if fs.pathExists(path) then fs.write(path, selected.code) + else fs.createFile(path, selected.code, forceOptions()) + this.dispatchEvent(new CustomEvent("file-open", fileOpenOptions(path, path.split("/").pop()))) + + fun attachExampleRows() = + let panel = this + this.querySelectorAll(".mock-example-row").forEach of (button, ...) => + button.addEventListener("click", () => panel.selectExample(button.dataset.exampleId)) + this.querySelectorAll(".mock-load-example-button").forEach of (button, ...) => + button.addEventListener("click", () => panel.loadExample(button.dataset.exampleId)) + +customElements.define("source-control-panel", SourceControlPanel) +customElements.define("examples-panel", ExamplesPanel) diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/components/NativeDialogs.mls b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/NativeDialogs.mls new file mode 100644 index 0000000000..46784b0442 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/NativeDialogs.mls @@ -0,0 +1,721 @@ +import "../filesystem/fs.mls" +import "../filesystem/projects.mls" +import "../common/JS.mls" +import "../common/Zip.mls" +import "../common/DialogHelpers.mls" + +open JS { Absent } + +fun escapeHtml(text) = + let div = document.createElement("div") + set div.textContent = text + div.innerHTML + +// Shared with the project switcher's Export button β€” every archive we emit +// carries a manifest at this path so consumers know which project produced +// it and so re-import via the switcher can reconstruct the project metadata. +val EXPORT_MANIFEST_PATH = "manifest.json" +val EXPORT_SCHEMA = "mlscript-project/v2" + +fun manifestEntry(project) = + let + payload = mut + schema: EXPORT_SCHEMA + exportedAt: new Date().toISOString() + project: project + content = JSON.stringify(payload, null, 2) + mut { path: EXPORT_MANIFEST_PATH, :content } + +class CommandPaletteDialog extends HTMLElement with + let query = "" + let activeCommandId = "" + let activeSymbolId = "" + let symbolIndex = null + let resultLimit = 100 + + fun connectedCallback() = + if window.__mlscriptSymbolIndex is ~Absent as index do set symbolIndex = index + this.render() + this.attachEventListeners() + renderPalette() + + fun render() = + set this.innerHTML = """ + +
+ +
+
+
+ """ + + fun command(id, title, detail, icon, disabled, disabledTitle) = + mut { :id, :title, :detail, :icon, :disabled, :disabledTitle } + + fun commands() = + [ + command("new-file", "New File", "Create a file in the workspace.", "icon-file-plus", false, "") + command("compile", "Compile current file", "Run the compiler for the active MLscript file.", "icon-binary", false, "") + command("execute", "Run compiled output", "Execute the active compiled program.", "icon-play", false, "") + // command("open-generated", "Open generated output", "Open the compiled-output split view.", "icon-columns-3", false, "") + // command("target-wasm", "Change compile target", "Open the split view and select the wasm mock target.", "icon-box", false, "") + command("go-local-symbol", "Go to Symbol in File", "Search symbols in the active file.", "icon-at-sign", false, "") + command("go-workspace-symbol", "Go to Symbol in Workspace", "Search structural symbols across the workspace. Cmd+T / Ctrl+T.", "icon-hash", false, "") + command("open-outline", "Open Outline panel", "Open the Outline activity panel.", "icon-list-tree", false, "") + command("go-file", "Go to file", "Open the Files activity panel.", "icon-files", false, "") + command("search-files", "Search across files", "Open the Search activity panel.", "icon-search", false, "") + command("toggle-problems", "Toggle Problems panel", "Show or hide the Problems panel.", "icon-circle-alert", false, "") + command("toggle-output", "Toggle output panel", "Show or hide the bottom panel.", "icon-panel-bottom", false, "") + command("clear-output", "Clear Output", "Clear the Output tab.", "icon-square-x", false, "") + command("clear-logs", "Clear Logs", "Clear the Logging tab.", "icon-square-x", false, "") + command("export-project", "Export Project", "Open project export options.", "icon-download", false, "") + command("toggle-theme", "Toggle theme", "Future visual command.", "icon-sun-moon", true, "Theme switching is mocked in Phase 7.") + ] + + fun normalizedQuery() = + query.trim().toLowerCase() + + fun commandMatches(item, needle) = if + needle is "" then true + item.title.toLowerCase().includes(needle) then true + item.detail.toLowerCase().includes(needle) then true + else false + + fun filteredCommands() = + let needle = normalizedQuery() + commands().filter(item => commandMatches(item, needle)) + + fun paletteMode() = if + query.startsWith("@") then "local-symbols" + query.startsWith("#") then "global-symbols" + else "commands" + + fun commandRowHtml(item) = + let + disabledAttr = if item.disabled then " disabled aria-disabled=\"true\"" else "" + titleAttr = if item.disabled then item.disabledTitle else item.detail + disabledClass = if item.disabled then " disabled" else "" + activeClass = if item.id === activeCommandId then " active" else "" + selectedAttr = if item.id === activeCommandId then "true" else "false" + """ + + """ + + fun selectableCommands(items) = + items.filter(item => not item.disabled) + + fun firstSelectableId(items) = + let id = "" + selectableCommands(items).forEach of (item, ...) => + if id === "" do set id = item.id + id + + fun activeCommandExists(items) = + let hasActiveCommand = false + selectableCommands(items).forEach of (item, ...) => + if item.id === activeCommandId do set hasActiveCommand = true + hasActiveCommand + + fun ensureActiveCommand(items) = + if not activeCommandExists(items) do set activeCommandId = firstSelectableId(items) + + fun renderCommands() = + if this.querySelector(".command-list") is ~null as list do + let matching = filteredCommands() + ensureActiveCommand(matching) + list.setAttribute("aria-label", "Commands") + if matching.length === 0 then + set list.innerHTML = """
No commands
""" + else + set list.innerHTML = matching.map(item => commandRowHtml(item)).join("") + attachCommandRows() + + fun searchInput() = + this.querySelector(".command-search-input") + + fun indexedActiveFilePath() = if + symbolIndex is null then null + symbolIndex.activeFile is Absent then null + else symbolIndex.activeFile + + fun activeFilePath() = + let activePath = getActiveFilePath() + if activePath is + null then indexedActiveFilePath() + else activePath + + fun localIndexTargetsActiveFile() = + let + activePath = activeFilePath() + indexedPath = indexedActiveFilePath() + if activePath is + null then true + else activePath === indexedPath + + fun localIndexIsStale() = + activeFileIsMls() and not localIndexTargetsActiveFile() + + fun activeFileIsMls() = + if activeFilePath() is + null then false + path then typeof(path) is "string" and path.endsWith(".mls") + + fun localEntries() = if + symbolIndex is null then [] + symbolIndex.localEntries is Absent then [] + not localIndexTargetsActiveFile() then [] + else symbolIndex.localEntries + + fun globalEntries() = if + symbolIndex is null then [] + symbolIndex.globalEntries is Absent then [] + else symbolIndex.globalEntries + + fun symbolModeEntries() = if paletteMode() is + "local-symbols" then localEntries() + "global-symbols" then globalEntries() + else [] + + fun symbolNeedle() = + query.slice(1).trim().toLowerCase() + + fun symbolTokens() = + let needle = symbolNeedle() + if needle === "" then [] + else needle.split(new RegExp("\\s+")).filter(token => token !== "") + + fun entryText(value, fallback) = if + value is Absent then fallback + value is null then fallback + else "" + value + + fun numberValue(value, fallback) = if + typeof(value) is "number" then value + else fallback + + fun symbolIndexStatus() = if + symbolIndex is null then "waiting" + symbolIndex.status is Absent then "ready" + else entryText(symbolIndex.status, "ready") + + fun indexedFileCount() = if + symbolIndex is null then 0 + else numberValue(symbolIndex.indexedFileCount, 0) + + fun totalFileCount() = if + symbolIndex is null then 0 + else numberValue(symbolIndex.totalFileCount, 0) + + fun symbolIndexProgressSuffix() = + let + indexed = indexedFileCount() + total = totalFileCount() + if total > 0 then " (" + indexed + " of " + total + " files indexed)" + else "" + + fun ownerPath(entry) = if + entry.ownerPath is Absent then [] + entry.ownerPath is null then [] + else entry.ownerPath + + fun ownerText(entry) = + ownerPath(entry).join(" > ") + + fun entrySearchText(entry) = + if entry.searchText is ~Absent as search then search + else [ + entryText(entry.name, "") + entryText(entry.kind, "symbol") + entryText(entry.origin, "source") + entryText(entry.filePath, "") + ownerText(entry) + ].join(" ").toLowerCase() + + fun entryMatches(entry, tokens) = + tokens.every(token => entrySearchText(entry).toLowerCase().includes(token)) + + fun symbolScore(entry, tokens) = + if tokens.length === 0 then 0 + else + let + first = tokens.at(0) + name = entryText(entry.name, "").toLowerCase() + if name.startsWith(symbolNeedle()) then 0 + else if name.startsWith(first) then 0 + else if ownerText(entry).toLowerCase().includes(first) then 1 + else if entryText(entry.filePath, "").toLowerCase().includes(first) then 2 + else 3 + + fun compareSymbolResults(left, right) = + if left.score !== right.score then left.score - right.score + else entryText(left.entry.name, "").localeCompare(entryText(right.entry.name, "")) + + fun filteredSymbolEntries() = + let tokens = symbolTokens() + symbolModeEntries() + .filter(entry => entryMatches(entry, tokens)) + .map(entry => mut { entry: entry, score: symbolScore(entry, tokens) }) + .sort((left, right) => compareSymbolResults(left, right)) + .map(result => result.entry) + + fun activeSymbolExists(entries) = + let found = false + entries.forEach of (entry, ...) => + if entry.stableId === activeSymbolId do set found = true + found + + fun ensureActiveSymbol(entries) = + if not activeSymbolExists(entries) do + set activeSymbolId = if entries.length === 0 then "" else entries.at(0).stableId + + fun kindLabel(kind) = if kind is + "function" then "fn" + "module" then "mod" + "object" then "obj" + "class" then "cls" + "parameter" then "arg" + "mutable-value" then "mut" + "constructor" then "ctor" + "value" then "val" + "pattern" then "pat" + "trait" then "trt" + "mixin" then "mix" + "type" then "typ" + "field" then "fld" + else kind + + fun symbolKindClass(kind) = if kind is + "file" then "outline-kind-file" + "module" then "outline-kind-module" + "class" then "outline-kind-class" + "object" then "outline-kind-object" + "function" then "outline-kind-function" + "value" then "outline-kind-value" + "mutable-value" then "outline-kind-mutable-value" + "parameter" then "outline-kind-parameter" + "constructor" then "outline-kind-constructor" + else "outline-kind-symbol" + + fun originHtml(entry) = + let origin = entryText(entry.origin, "source") + if origin === "source" then "" + else """""" + escapeHtml(origin) + """""" + + fun symbolDetail(entry) = + let + owner = ownerText(entry) + file = entryText(entry.filePath, "") + line = if entry.line is Absent then 1 else entry.line + location = file + ":" + line + if owner === "" then location + else owner + " - " + location + + fun symbolRowHtml(entry) = + let + activeClass = if entry.stableId === activeSymbolId then " active" else "" + selectedAttr = if entry.stableId === activeSymbolId then "true" else "false" + kind = entryText(entry.kind, "symbol") + """ + + """ + + fun symbolCountHtml(total, shown) = + if total <= shown then "" + else """
Showing """ + shown + """ of """ + total + """ symbols
""" + + fun symbolStatusText(mode) = if + mode is "local-symbols" and localIndexIsStale() then "Indexing current file symbols" + mode is "global-symbols" and symbolIndexStatus() === "indexing" then "Indexing workspace symbols" + symbolIndexProgressSuffix() + else "" + + fun symbolStatusHtml(mode) = + let text = symbolStatusText(mode) + if text === "" then "" + else """
""" + escapeHtml(text) + """
""" + + fun symbolEmptyText(mode) = if + symbolIndex is null then "Waiting for symbol index" + mode is "local-symbols" and not activeFileIsMls() then "Open an MLscript file to browse symbols" + mode is "local-symbols" and localIndexIsStale() then "Indexing current file symbols" + mode is "global-symbols" and symbolIndexStatus() === "indexing" then "Indexing workspace symbols" + symbolIndexProgressSuffix() + symbolTokens().length > 0 then "No matching symbols" + mode is "local-symbols" then "No symbols in current file" + else "No workspace symbols" + + fun renderSymbols() = + if this.querySelector(".command-list") is ~null as list do + let + mode = paletteMode() + matching = filteredSymbolEntries() + visible = matching.slice(0, resultLimit) + ensureActiveSymbol(visible) + list.setAttribute("aria-label", if mode is "local-symbols" then "File symbols" else "Workspace symbols") + if matching.length === 0 then + set list.innerHTML = """
""" + escapeHtml(symbolEmptyText(mode)) + """
""" + else + set list.innerHTML = symbolStatusHtml(mode) + symbolCountHtml(matching.length, visible.length) + visible.map(entry => symbolRowHtml(entry)).join("") + attachSymbolRows() + + fun renderPalette() = + if paletteMode() is + "commands" then renderCommands() + else renderSymbols() + + fun dialog() = + this.querySelector("dialog") + + fun openPalette() = + set + query = "" + activeCommandId = "" + activeSymbolId = "" + renderPalette() + if dialog() is ~null as commandDialog do + if not commandDialog.open do commandDialog.showModal() + if searchInput() is ~null as input do + set input.value = "" + window.setTimeout(() => input.focus(), 0) + + fun openSymbolPalette(prefix) = + set + query = prefix + activeCommandId = "" + activeSymbolId = "" + renderPalette() + if dialog() is ~null as commandDialog do + if not commandDialog.open do commandDialog.showModal() + if searchInput() is ~null as input do + set input.value = prefix + window.setTimeout( + () => + input.focus() + input.setSelectionRange(prefix.length, prefix.length), + 0 + ) + + fun closePalette() = + if dialog() is ~null as commandDialog do + if commandDialog.open do commandDialog.close() + + fun getActiveFilePath() = + let editorPanel = document.querySelector("editor-panel") + if + editorPanel is Absent then null + editorPanel.activeTabId is Absent then null + editorPanel.openTabs is Absent then null + else + let activeTab = editorPanel.openTabs.get(editorPanel.activeTabId) + if activeTab is + Absent then null + ~Absent as tab then tab.path + + fun fileEventDetail() = + mut { filePath: getActiveFilePath() } + + fun fileEventOptions() = + mut { bubbles: true, detail: fileEventDetail() } + + fun sidebarDetail(side, panel) = + mut { :side, :panel } + + fun sidebarEventOptions(side, panel) = + mut { detail: sidebarDetail(side, panel) } + + fun dispatchSidebar(side, panel) = + document.dispatchEvent(new CustomEvent("sidebar-toggle-requested", sidebarEventOptions(side, panel))) + + fun dispatchSidebarOpen(side, panel) = + document.dispatchEvent(new CustomEvent("sidebar-toggle-requested", mut + detail: mut { :side, :panel, forceOpen: true } + )) + + fun editorWorkbench() = + document.querySelector("editor-workbench") + + fun bottomPanel() = + document.querySelector("bottom-panel") + + fun requestNewFile() = + dispatchSidebarOpen("left", "files") + document.dispatchEvent(new CustomEvent("new-file-requested")) + + fun openGeneratedOutput(target) = + if editorWorkbench() is ~null as shell do + shell.openCompiledOutput() + shell.setTarget(target) + + fun executeCommand(id) = + if id is + "new-file" then requestNewFile() + "compile" then document.dispatchEvent(new CustomEvent("compile-requested", fileEventOptions())) + "execute" then document.dispatchEvent(new CustomEvent("execute-requested", fileEventOptions())) + // "open-generated" then openGeneratedOutput("mjs") + // "target-wasm" then openGeneratedOutput("wasm") + "go-local-symbol" then openSymbolPalette("@") + "go-workspace-symbol" then openSymbolPalette("#") + "open-outline" then dispatchSidebar("left", "outline") + "go-file" then dispatchSidebar("left", "files") + "search-files" then dispatchSidebar("left", "search") + "toggle-problems" then dispatchSidebar("right", "problems") + "toggle-output" then + if bottomPanel() is ~null as panel do panel.toggleCollapse() + "clear-output" then + if bottomPanel() is ~null as panel do panel.clearOutput() + "clear-logs" then + if bottomPanel() is ~null as panel do panel.clearLogs() + "export-project" then document.dispatchEvent(new CustomEvent("share-dialog-open-requested")) + else () + if id !== "go-local-symbol" and id !== "go-workspace-symbol" do closePalette() + + fun isOpenShortcut(event) = + let isMac = window.navigator.platform.toUpperCase().indexOf("MAC") >= 0 + let isCmdOrCtrl = if isMac then event.metaKey else event.ctrlKey + isCmdOrCtrl and isKey(event, "k", "K") + + fun isWorkspaceSymbolShortcut(event) = + let isMac = window.navigator.platform.toUpperCase().indexOf("MAC") >= 0 + let isCmdOrCtrl = if isMac then event.metaKey else event.ctrlKey + isCmdOrCtrl and isKey(event, "t", "T") + + fun isKey(event, lower, upper) = if + event.key === lower then true + event.key === upper then true + else false + + fun activeCommandIndex(items) = + let index = -1 + items.forEach of (item, itemIndex) => + if item.id === activeCommandId do set index = itemIndex + index + + fun syncActiveCommand(shouldScroll) = + this.querySelectorAll(".command-row").forEach of (button, ...) => + let isActive = button.dataset.commandId === activeCommandId + button.classList.toggle("active", isActive) + button.setAttribute("aria-selected", if isActive then "true" else "false") + if isActive and shouldScroll do button.scrollIntoView(mut { block: "nearest" }) + + fun moveActiveCommand(delta) = + let items = selectableCommands(filteredCommands()) + if items.length > 0 do + let currentIndex = activeCommandIndex(items) + let nextIndex = if currentIndex < 0 then 0 else (currentIndex + delta + items.length) % items.length + set activeCommandId = items.at(nextIndex).id + syncActiveCommand(true) + + fun activeSymbolIndex(items) = + let index = -1 + items.forEach of (item, itemIndex) => + if item.stableId === activeSymbolId do set index = itemIndex + index + + fun syncActiveSymbol(shouldScroll) = + this.querySelectorAll(".command-symbol-row").forEach of (button, ...) => + let isActive = button.dataset.symbolId === activeSymbolId + button.classList.toggle("active", isActive) + button.setAttribute("aria-selected", if isActive then "true" else "false") + if isActive and shouldScroll do button.scrollIntoView(mut { block: "nearest" }) + + fun moveActiveSymbol(delta) = + let items = filteredSymbolEntries().slice(0, resultLimit) + if items.length > 0 do + let currentIndex = activeSymbolIndex(items) + let nextIndex = if currentIndex < 0 then 0 else (currentIndex + delta + items.length) % items.length + set activeSymbolId = items.at(nextIndex).stableId + syncActiveSymbol(true) + + fun moveActiveItem(delta) = + if paletteMode() is + "commands" then moveActiveCommand(delta) + else moveActiveSymbol(delta) + + fun executeActiveCommand() = + let items = filteredCommands() + ensureActiveCommand(items) + if activeCommandId !== "" do executeCommand(activeCommandId) + + fun executeSymbolEntry(entry) = + document.dispatchEvent(new CustomEvent("open-file-at-location", mut + detail: mut + filePath: entryText(entry.filePath, "") + line: if entry.line is Absent then 1 else entry.line + column: if entry.column is Absent then 0 else entry.column + length: if entry.length is Absent then 0 else entry.length + )) + closePalette() + + fun activeSymbolEntry(items) = + let found = null + items.forEach of (entry, ...) => + if found is null and entry.stableId === activeSymbolId do set found = entry + found + + fun symbolEntryById(id, items) = + let found = null + items.forEach of (entry, ...) => + if found is null and entry.stableId === id do set found = entry + found + + fun executeActiveSymbol() = + let items = filteredSymbolEntries().slice(0, resultLimit) + ensureActiveSymbol(items) + if activeSymbolEntry(items) is ~null as entry do executeSymbolEntry(entry) + + fun executeActiveItem() = + if paletteMode() is + "commands" then executeActiveCommand() + else executeActiveSymbol() + + fun handleSearchKeydown(event) = if event.key is + "ArrowDown" then + event.preventDefault() + moveActiveItem(1) + "ArrowUp" then + event.preventDefault() + moveActiveItem(-1) + "Enter" then + event.preventDefault() + executeActiveItem() + else () + + fun attachCommandRows() = + let palette = this + this.querySelectorAll(".command-row").forEach of (button, ...) => + if not button.disabled do + button.addEventListener("click", () => palette.executeCommand(button.dataset.commandId)) + button.addEventListener of "mouseenter", () => + set palette.activeCommandId = button.dataset.commandId + palette.syncActiveCommand(false) + + fun attachSymbolRows() = + let palette = this + this.querySelectorAll(".command-symbol-row").forEach of (button, ...) => + button.addEventListener of "click", () => + if palette.symbolEntryById(button.dataset.symbolId, palette.filteredSymbolEntries()) is ~null as entry do palette.executeSymbolEntry(entry) + button.addEventListener of "mouseenter", () => + set palette.activeSymbolId = button.dataset.symbolId + palette.syncActiveSymbol(false) + + fun handleSymbolIndexUpdated(detail) = + if detail is ~Absent as payload do + if payload.index is ~Absent as index do set symbolIndex = index + if paletteMode() is + "commands" then () + else renderPalette() + + fun attachEventListeners() = + let palette = this + if dialog() is ~null as commandDialog do DialogHelpers.closeDismissibleDialogFromBackdrop(commandDialog) + document.addEventListener("command-palette-open-requested", () => palette.openPalette()) + document.addEventListener("analysis-symbol-index-updated", event => palette.handleSymbolIndexUpdated(event.detail)) + document.addEventListener of "keydown", event => + if palette.isOpenShortcut(event) do + event.preventDefault() + palette.openPalette() + if palette.isWorkspaceSymbolShortcut(event) do + event.preventDefault() + palette.openSymbolPalette("#") + if searchInput() is ~null as input do + input.addEventListener("keydown", event => palette.handleSearchKeydown(event)) + input.addEventListener of "input", event => + set query = event.target.value + set + activeCommandId = "" + activeSymbolId = "" + renderPalette() + +class ShareDialog extends HTMLElement with + + fun connectedCallback() = + this.render() + this.attachEventListeners() + + fun render() = + set this.innerHTML = """ + + """ + + fun dialog() = + this.querySelector("dialog") + + fun openDialog() = + if dialog() is ~null as shareDialog do + if not shareDialog.open do shareDialog.showModal() + if this.querySelector(".share-download-button") is ~null as downloadButton do + window.setTimeout(() => downloadButton.focus(), 0) + + fun allFilesFilter(path) = + true + + fun mlsFilesFilter(path) = + path.endsWith(".mls") + + fun javascriptFilesFilter(path) = + if path.endsWith(".mjs") then true + else path.endsWith(".js") + + fun fileEntries(fileFilter) = + let + files = fs.getAllFiles(undefined) + paths = Object.keys(files).filter(path => fileFilter(path)).sort() + entries = mut [] + paths.forEach of (path, ...) => + entries.push(mut { :path, content: files.(path) }) + entries + + fun workspaceZipBytes(fileFilter) = + let + entries = mut [manifestEntry(projects.currentProject())] + files = fileEntries(fileFilter) + files.forEach of (entry, ...) => entries.push(entry) + Zip.build(entries) + + fun downloadZip(fileFilter, filename) = + Zip.download(workspaceZipBytes(fileFilter), filename, "application/zip") + if dialog() is ~null as shareDialog do shareDialog.close() + + fun attachEventListeners() = + let share = this + if dialog() is ~null as shareDialog do DialogHelpers.closeDismissibleDialogFromBackdrop(shareDialog) + document.addEventListener("share-dialog-open-requested", () => share.openDialog()) + if this.querySelector(".share-download-all-button") is ~null as downloadButton do + downloadButton.addEventListener("click", () => share.downloadZip(path => share.allFilesFilter(path), "mlscript-workspace.zip")) + if this.querySelector(".share-download-mls-button") is ~null as downloadMlsButton do + downloadMlsButton.addEventListener("click", () => share.downloadZip(path => share.mlsFilesFilter(path), "mlscript-files.zip")) + if this.querySelector(".share-download-js-button") is ~null as downloadJsButton do + downloadJsButton.addEventListener("click", () => share.downloadZip(path => share.javascriptFilesFilter(path), "javascript-files.zip")) + +customElements.define("command-palette-dialog", CommandPaletteDialog) +customElements.define("share-dialog", ShareDialog) diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/components/OutlinePanel.mls b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/OutlinePanel.mls new file mode 100644 index 0000000000..510c192dec --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/OutlinePanel.mls @@ -0,0 +1,261 @@ +import "../analysis/SymbolTree.mls" +import "../common/JS.mls" + +open JS { Absent } + +fun escapeHtml(text) = + let div = document.createElement("div") + set div.textContent = text + div.innerHTML + +class OutlinePanel extends HTMLElement with + let + documentData = null + status = "loading" + errorText = "" + expandedKeys = new Set() + collapsedKeys = new Set() + selectedKey = "" + + fun connectedCallback() = + this.restoreLatestDocument() + this.render() + this.attachEventListeners() + + fun restoreLatestDocument() = + if window.__mlscriptAnalysisDocument is ~Absent as document do + set + documentData = SymbolTree.normalizeDocument(document, document.revision) + status = "ready" + + fun root() = if + documentData is null then null + documentData.root is Absent then null + else documentData.root + + fun render() = + set this.innerHTML = """ +
+
+

Outline

+
+
+ """ + treeHtml() + """ +
+
+ """ + attachNodeListeners() + + fun unavailableText() = + if errorText === "" then "Outline is not available" else errorText + + fun treeHtml() = if + status is "error" then """
""" + escapeHtml(unavailableText()) + """
""" + root() is null then """
No symbols
""" + else nodeHtml(root(), 0) + + fun nodeKey(node) = + SymbolTree.stableExpansionKey(node, documentData.rootFile) + + fun nodeLocationLabel(node) = + let detail = SymbolTree.rangeToNavigationDetail(node, documentData.rootFile) + "L" + detail.line + + fun hasChildren(node) = + node.children is ~Absent and node.children.length > 0 + + fun iconFor(kind) = if kind is + "file" then "icon-file-code" + "module" then "icon-box" + "class" then "icon-boxes" + "object" then "icon-circle-dot" + "function" then "icon-braces" + "type" then "icon-type" + "pattern" then "icon-regex" + "value" then "icon-variable" + else "icon-dot" + + fun kindLabel(kind) = if kind is + "function" then "fun" + "module" then "mod" + "object" then "obj" + "class" then "cls" + "pattern" then "pat" + "type" then "typ" + "parameter" then "arg" + "mutable-value" then "mut" + "constructor" then "ctor" + "value" then "val" + else kind + + fun nodeKindClass(kind) = if kind is + "file" then "outline-kind-file" + "module" then "outline-kind-module" + "class" then "outline-kind-class" + "object" then "outline-kind-object" + "function" then "outline-kind-function" + "type" then "outline-kind-type" + "pattern" then "outline-kind-pattern" + "value" then "outline-kind-value" + "mutable-value" then "outline-kind-mutable-value" + "parameter" then "outline-kind-parameter" + "constructor" then "outline-kind-constructor" + else "outline-kind-symbol" + + fun nodeMainHtml(node) = + """ + """ + escapeHtml(kindLabel(node.kind)) + """ + + """ + escapeHtml(node.name) + """ + """ + escapeHtml(nodeLocationLabel(node)) + """ + """ + + fun caretHtml(node) = + """""" + + fun spacerHtml() = + """""" + + fun childNodesHtml(node, depth) = + node.children.map(child => nodeHtml(child, depth + 1)).join("") + + fun defaultOpen(depth) = + depth < 3 + + fun isExpanded(key, depth) = + if collapsedKeys.has(key) then false + else if expandedKeys.has(key) then true + else defaultOpen(depth) + + fun nodeHtml(node, depth) = + let + key = nodeKey(node) + selectedClass = if selectedKey === key then " selected" else "" + kindClass = nodeKindClass(node.kind) + depthStyle = " style=\"--outline-depth: " + depth + "\"" + if hasChildren(node) then + let openAttr = if isExpanded(key, depth) then " open" else "" + """ +
+ + """ + caretHtml(node) + """ + """ + nodeMainHtml(node) + """ + +
+ """ + childNodesHtml(node, depth) + """ +
+
+ """ + else + """ + + """ + + fun findNodeByKey(key) = + let found = null + fun visit(node) = + if found is null do + if nodeKey(node) === key then set found = node + else if node.children is ~Absent do + node.children.forEach of (child, ...) => visit(child) + if root() is ~null as rootNode do visit(rootNode) + found + + fun markSelected(element, key) = + set selectedKey = key + this.querySelectorAll(".selected").forEach of (selected, ...) => + selected.classList.remove("selected") + if element.classList.contains("outline-node-summary") then + if element.parentElement is ~Absent as parent do parent.classList.add("selected") + else element.classList.add("selected") + + fun dispatchOpenNode(key, element) = + if findNodeByKey(key) is ~null as node do + markSelected(element, key) + let detail = SymbolTree.rangeToNavigationDetail(node, documentData.rootFile) + document.dispatchEvent(new CustomEvent("open-file-at-location", mut { detail: detail })) + + fun handleDetailsToggle(details) = + let key = details.dataset.outlineKey + if details.open then + expandedKeys.add(key) + collapsedKeys.delete(key) + else + expandedKeys.delete(key) + collapsedKeys.add(key) + + fun toggleDetails(details) = + let key = details.dataset.outlineKey + if details.open then + set details.open = false + expandedKeys.delete(key) + collapsedKeys.add(key) + else + set details.open = true + expandedKeys.add(key) + collapsedKeys.delete(key) + + fun isCaretTarget(event) = + if event.target is + Absent then false + target then + if target.closest(".outline-node-caret") is + null then false + else true + + fun handleSummaryClick(event, summary) = + event.preventDefault() + if isCaretTarget(event) then + if summary.parentElement is ~Absent as details do toggleDetails(details) + else dispatchOpenNode(summary.dataset.outlineKey, summary) + + fun handleCaretKey(event, summary) = + if event.key is + "Enter" | " " then + event.preventDefault() + if summary.parentElement is ~Absent as details do toggleDetails(details) + else () + + fun attachNodeListeners() = + let panel = this + this.querySelectorAll(".outline-node").forEach of (details, ...) => + details.addEventListener("toggle", event => panel.handleDetailsToggle(details)) + this.querySelectorAll(".outline-node-summary").forEach of (summary, ...) => + summary.addEventListener("click", event => panel.handleSummaryClick(event, summary)) + if summary.querySelector(".outline-node-caret") is ~null as caret do + caret.addEventListener("keydown", event => panel.handleCaretKey(event, summary)) + this.querySelectorAll(".outline-node-leaf").forEach of (button, ...) => + button.addEventListener("click", event => panel.dispatchOpenNode(button.dataset.outlineKey, button)) + + fun setDocument(document) = + set + documentData = SymbolTree.normalizeDocument(document, document.revision) + status = "ready" + errorText = "" + render() + + fun setStatus(detail) = + if detail.status is + "loading" then + set status = "loading" + render() + "error" then + set + status = "error" + documentData = null + errorText = if detail.error is Absent then "Analysis error" else detail.error.message + render() + "ready" then + set status = "ready" + render() + else () + + fun attachEventListeners() = + let panel = this + document.addEventListener("analysis-document-updated", event => panel.setDocument(event.detail.document)) + document.addEventListener("analysis-status-changed", event => panel.setStatus(event.detail)) + +customElements.define("outline-panel", OutlinePanel) diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/components/PanelPersistence.mls b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/PanelPersistence.mls new file mode 100644 index 0000000000..264580cb47 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/PanelPersistence.mls @@ -0,0 +1,51 @@ +import "../common/JS.mls" + +open JS { Absent } + +module PanelPersistence with... + +fun panelWidthCssVar(panel) = + "--" + panel.tagName.toLowerCase() + "-width" + +fun restorePanelWidth(panel, storageKey, minSize, maxSize) = + let savedWidth = localStorage.getItem(storageKey) + if savedWidth is ~Absent and savedWidth !== "" do + let width = parseInt(savedWidth, 10) + if width >= minSize and width <= maxSize do + let container = document.querySelector(".app-container") + container.style.setProperty(panelWidthCssVar(panel), width + "px") + panel.setAttribute("width", width) + +fun savePanelWidth(storageKey, width) = + localStorage.setItem(storageKey, width) + +fun restorePanelHeight(panel, storageKey, minSize, maxSize) = + let savedHeight = localStorage.getItem(storageKey) + if savedHeight is ~Absent and savedHeight !== "" do + let height = parseInt(savedHeight, 10) + if height >= minSize and height <= maxSize do + set + panel.style.height = height + "px" + panel.style.minHeight = height + "px" + panel.style.maxHeight = height + "px" + panel.setAttribute("height", height) + +fun savePanelHeight(storageKey, height) = + localStorage.setItem(storageKey, height) + +fun restoreNumber(storageKey, fallback, minSize, maxSize) = + let savedValue = localStorage.getItem(storageKey) + if savedValue is ~Absent and savedValue !== "" then + let value = parseInt(savedValue, 10) + if value >= minSize and value <= maxSize then value else fallback + else fallback + +fun saveNumber(storageKey, value) = + localStorage.setItem(storageKey, value) + +fun restoreString(storageKey, fallback) = + let savedValue = localStorage.getItem(storageKey) + if savedValue is ~Absent and savedValue !== "" then savedValue else fallback + +fun saveString(storageKey, value) = + localStorage.setItem(storageKey, value) diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/components/ProjectSwitcher.mls b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/ProjectSwitcher.mls new file mode 100644 index 0000000000..b8eb4a2a9b --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/ProjectSwitcher.mls @@ -0,0 +1,851 @@ +import "../filesystem/fs.mls" +import "../filesystem/persistent.mls" +import "../filesystem/projects.mls" +import "../common/JS.mls" +import "../common/Logger.mls" +import "../common/Zip.mls" +import "../common/DialogHelpers.mls" +import "../editor/editor.mls" + +open JS { Absent } + +// Format shared with the Share dialog. Both buttons emit a ZIP archive that +// contains `manifest.json` at the root plus the project files at their +// original paths (with the leading "/" stripped, as is conventional for zip). +val EXPORT_MANIFEST_PATH = "manifest.json" +val EXPORT_SCHEMA = "mlscript-project/v2" + +fun escapeHtml(text) = + let div = document.createElement("div") + set div.textContent = text + div.innerHTML + +fun closeOnBackdropClick(dialogEl) = + dialogEl.addEventListener of "click", event => + if dialogEl.dataset.dismissible === "true" and event.target === dialogEl do + dialogEl.close() + +fun glyphFor(project) = if + project is Absent then "Β·" + project.name is Absent then "Β·" + project.icon is ~Absent and project.icon !== "" then project.icon + else project.name.charAt(0).toUpperCase() + +fun relativeTime(stamp) = + if stamp is + Absent then "β€”" + text then + let + then_ = new Date(text) + now = new Date() + diffDays = Math.floor((now.getTime() - then_.getTime()) / 86400000) + if + diffDays <= 0 then "today" + diffDays === 1 then "yesterday" + diffDays < 7 then diffDays + "d ago" + diffDays < 30 then Math.floor(diffDays / 7) + "w ago" + diffDays < 365 then Math.floor(diffDays / 30) + "mo ago" + else Math.floor(diffDays / 365) + "y ago" + +fun filesLabel(count) = + count + " " + (if count === 1 then "file" else "files") + +// Std files are shared across projects but show up in every project's tree, so +// the displayed count should include them. +fun stdFileCount() = + let files = fs.getAllFiles((path, node) => node.attrs.("std") is true) + Object.keys(files).length + +fun totalFileCount(projectId) = + projects.fileCount(projectId) + stdFileCount() + +class ProjectSwitcher extends HTMLElement with + let + selectedId = "" + pendingDelete = null + pendingRename = null + creating = false + createName = "" + previewPath = "" + importError = "" + collapsedFolders = new Set() + previewEditor = null + + fun connectedCallback() = + this.render() + this.attachEventListeners() + set selectedId = projects.currentId() + + fun render() = + set this.innerHTML = """ + +
+
+
+

Projects

+

Switch between projects or start a new one.

+
+ +
+
+ +
+
+
+ + +
+
+ +
+

New project

+ +

A blank project with /main.mls will be created.

+
+ + +
+
+
+ +
+

Import a project

+

+ Pick a previously-exported MLscript Web IDE archive. + The file is read entirely in your browser β€” nothing is uploaded. +

+
    +
  • Format: ZIP containing manifest.json with schema: "mlscript-project/v2".
  • +
  • Typical filename: <project>.zip (as produced by the Export or Share buttons).
  • +
  • Only uncompressed (stored) ZIPs are supported β€” archives re-zipped by an OS tool may use deflate and will not import.
  • +
  • Practical size limit: a few MB (your browser's localStorage quota).
  • +
  • The imported project gets a fresh id and appears at the bottom of the list. It does not open automatically.
  • +
+ + +
+ + +
+
+
+ +
+

Rename project

+ +
+ + +
+
+
+ +
+

Delete this project?

+

+ All persisted files for this project will be removed from your browser. This cannot be undone. +

+
+
+ + +
+
+
+
+ """ + + fun dialog() = + this.querySelector("dialog.project-switcher-native") + + fun subDialog(name) = + this.querySelector("dialog.project-switcher-sub[data-sub=\"" + name + "\"]") + + fun openDialog() = + set selectedId = projects.currentId() + set creating = false + set pendingDelete = null + this.renderList() + this.renderDetail() + if dialog() is ~null as d do + DialogHelpers.showDialogWithTransition(d) + + fun close() = + if dialog() is ~null as d do + DialogHelpers.closeDialogWithTransition(d) + + fun cardHtml(project) = + let + sel = project.id === selectedId + current = projects.isCurrent(project.id) + count = totalFileCount(project.id) + classes = "project-switcher-card" + + (if sel then " sel" else "") + + (if current then " current" else "") + pill = if current then """Open""" else "" + """ + + """ + + fun renderList() = + if this.querySelector(".project-switcher-list") is ~null as list do + let cards = projects.listProjects().map(p => cardHtml(p)) + set list.innerHTML = cards.join("") + attachListListeners(list) + + fun emptyDetailHtml() = + """

No project selected.

""" + + fun detailHtml(project) = + let + current = projects.isCurrent(project.id) + count = totalFileCount(project.id) + openLabel = if current then "Currently open" else "Open project" + openClass = "project-switcher-open" + (if current then " is-current" else "") + openDisabled = if current then " disabled aria-disabled=\"true\"" else "" + isOnlyOne = projects.listProjects().length <= 1 + deleteDisabled = if current then " disabled aria-disabled=\"true\"" else if isOnlyOne then " disabled aria-disabled=\"true\"" else "" + description = if project.description is ~Absent and project.description !== "" then project.description else "β€”" + """ +
+ """ + escapeHtml(glyphFor(project)) + """ +
+

""" + escapeHtml(project.name) + """

+

""" + escapeHtml(description) + """

+
+
+
+ + + + +
+
+
Files
""" + count + """
+
Created
""" + escapeHtml(project.createdAt) + """
+
Modified
""" + escapeHtml(relativeTime(project.modifiedAt)) + """
+
ID
""" + escapeHtml(project.id) + """
+
+
+
+
+
+ + +
+
+
+
+ """ + + fun renderDetail() = + if this.querySelector(".project-switcher-detail") is ~null as detail do + let project = projects.findProject(selectedId) + if project is + Absent do set detail.innerHTML = emptyDetailHtml() + ~Absent as p do + set detail.innerHTML = detailHtml(p) + attachDetailListeners(detail, p) + this.renderPreview(detail, p) + + fun pathParts(path) = + path.split("/").filter of (s, ...) => s !== "" + + fun buildTreeRows(paths) = + let + rows = mut [] + emittedFolders = new Set() + paths.forEach of (path, ...) => + let + parts = pathParts(path) + partsCount = parts.length + dirCount = if partsCount > 0 then partsCount - 1 else 0 + filename = if partsCount > 0 then parts.at(partsCount - 1) else "" + folderPath = "" + i = 0 + while i < dirCount do + let segment = parts.at(i) + set folderPath = folderPath + "/" + segment + if not emittedFolders.has(folderPath) do + emittedFolders.add(folderPath) + rows.push(mut + 'type: "folder" + name: segment + path: folderPath + depth: i + ) + set i += 1 + rows.push(mut + 'type: "file" + name: filename + path: path + depth: dirCount + ) + rows + + // Match the left-sidebar file explorer's icon choice: code-style icon for + // .mls / .mjs / .js sources. `icon-file-json` is not part of the loaded + // Lucide set, which is why .mjs rows used to render with no glyph at all. + fun fileIconFor(name) = if + name.endsWith(".mls") then "icon-file-code" + name.endsWith(".mjs") then "icon-file-code" + name.endsWith(".js") then "icon-file-code" + name.endsWith(".md") then "icon-file-code" + else "icon-file" + + fun fileRowHtml(row, activePath) = + let + active = row.path === activePath + activeClass = if active then " active" else "" + selectedAttr = if active then "true" else "false" + indentPx = 8 + row.depth * 14 + icon = fileIconFor(row.name) + """ + + """ + + fun folderRowHtml(row) = + let + indentPx = 8 + row.depth * 14 + isCollapsed = collapsedFolders.has(row.path) + chevronIcon = if isCollapsed then "icon-chevron-right" else "icon-chevron-down" + folderIcon = if isCollapsed then "icon-folder" else "icon-folder-open" + """ + + """ + + fun previewRowHtml(row, activePath) = if + row.'type === "folder" then folderRowHtml(row) + else fileRowHtml(row, activePath) + + // A row is hidden if any of its ancestor folders is currently collapsed. + fun isHiddenByCollapse(path) = + let + parts = pathParts(path) + limit = parts.length - 1 + i = 0 + result = false + while i < limit do + if result is false do + let ancestor = "/" + parts.slice(0, i + 1).join("/") + if collapsedFolders.has(ancestor) do set result = true + set i += 1 + result + + fun toggleFolder(path) = + if collapsedFolders.has(path) then collapsedFolders.delete(path) + else collapsedFolders.add(path) + + // Setter routed through a method so cross-scope writes (e.g. from a click + // lambda captured as `panel`) hit the private class field, not a fresh + // public property. + fun setPreviewPath(path) = + set previewPath = path + + fun collectStdPaths() = + let files = fs.getAllFiles((path, node) => node.attrs.("std") is true) + Object.keys(files) + + fun dedupedSortedPaths(userPaths) = + let combined = new Set(userPaths.concat(collectStdPaths())) + let result = Array.from(combined) + result.sort() + result + + fun renderPreview(detail, project) = + let + treeEl = detail.querySelector(".project-switcher-preview-tree") + headEl = detail.querySelector(".project-switcher-preview-head") + bodyEl = detail.querySelector(".project-switcher-preview-body") + snapshot = persistent.snapshotProject(project.id) + userPaths = snapshot.map((entry, ...) => entry.path) + paths = dedupedSortedPaths(userPaths) + let hasPreviewPath = paths.some of (p, ...) => p === previewPath + if not hasPreviewPath do + set previewPath = if paths.length > 0 then paths.at(0) else "" + if treeEl is ~null as t do + if paths.length === 0 then + set t.innerHTML = """
No files in this project.
""" + else + let + rows = buildTreeRows(paths) + visibleRows = rows.filter of (row, ...) => not isHiddenByCollapse(row.path) + set t.innerHTML = visibleRows.map(r => previewRowHtml(r, previewPath)).join("") + attachPreviewListeners(t, project) + let entry = previewEntry(snapshot, previewPath) + if headEl is ~null as h do renderPreviewHead(h, previewPath, entry.mtime) + if bodyEl is ~null as b do renderPreviewBody(b, entry.content, previewPath) + + // Returns { content, mtime }. Snapshot wins (user files); otherwise we fall + // through to fs (used for std files, which live in the in-memory tree and + // are not persisted). + fun previewEntry(snapshot, path) = + let result = null + snapshot.forEach of (e, ...) => + if result is null and e.path === path do + set result = mut + content: e.payload.content + mtime: e.payload.mtime + if result is ~null then result + else + let node = fs.findNode(path) + if node is + null then mut { content: "", mtime: null } + ~null as n then + if n.'type === "file" then + mut + content: if n.content is Absent then "" else n.content + mtime: n.mtime + else mut { content: "", mtime: null } + + fun extensionOf(path) = + let dot = path.lastIndexOf(".") + if dot < 0 then "" else path.slice(dot + 1) + + fun formatMtime(stamp) = if + stamp is Absent then "" + stamp is null then "" + else new Date(stamp).toLocaleString() + + fun renderPreviewHead(headEl, path, mtime) = + if headEl.querySelector(".project-switcher-preview-path") is ~null as pathEl do + set pathEl.textContent = path + if headEl.querySelector(".project-switcher-preview-mtime") is ~null as mtimeEl do + let stamp = if path === "" then null else mtime + set mtimeEl.textContent = formatMtime(stamp) + + // Mount or reuse a read-only CodeMirror EditorView in `bodyEl`. Rebuild only + // when the visible file changes; folder toggle re-renders use the same view. + fun renderPreviewBody(bodyEl, content, path) = + if path is "" then + if previewEditor is ~null as e do e.destroy() + set + previewEditor = null + bodyEl.innerHTML = "" + bodyEl.dataset.previewPath = "" + else + let needRebuild = if + previewEditor is null then true + not bodyEl.contains(previewEditor.dom) then true + bodyEl.dataset.previewPath !== path then true + else false + if needRebuild do + if previewEditor is ~null as e do e.destroy() + set + bodyEl.innerHTML = "" + bodyEl.dataset.previewPath = path + let extension = extensionOf(path) + set previewEditor = editor.createEditor(bodyEl, content, path, extension, true) + + fun attachPreviewListeners(treeEl, project) = + let panel = this + treeEl.querySelectorAll("button.ps-preview-file").forEach of (button, ...) => + button.addEventListener of "click", () => + panel.setPreviewPath(button.dataset.path) + let detail = panel.querySelector(".project-switcher-detail") + if detail is ~null as d do panel.renderPreview(d, project) + treeEl.querySelectorAll("button.ps-preview-folder").forEach of (button, ...) => + button.addEventListener of "click", () => + panel.toggleFolder(button.dataset.folderPath) + let detail = panel.querySelector(".project-switcher-detail") + if detail is ~null as d do panel.renderPreview(d, project) + + fun selectProject(id) = + set selectedId = id + this.renderList() + this.renderDetail() + + fun openProject(id) = + let project = projects.findProject(id) + if project is + Absent then () + ~Absent as p then + if projects.isCurrent(id) do close() + if not projects.isCurrent(id) do + document.dispatchEvent(new CustomEvent("project-open-requested", + mut { detail: mut { :id } })) + close() + + fun handleCreate() = + set creating = true + set createName = "" + let sub = subDialog("create") + if sub is ~null as d do + let input = d.querySelector("input[name=\"project-name\"]") + if input is ~null as field do set field.value = "" + if not d.open do d.showModal() + if input is ~null as field do + window.setTimeout(() => field.focus(), 0) + + fun confirmCreate() = + let + sub = subDialog("create") + input = if sub is Absent then null else sub.querySelector("input[name=\"project-name\"]") + raw = if input is Absent then "" else input.value + name = raw.trim() + if name is "" then () + else + let project = projects.createProject(name) + if sub is ~null as d do + if d.open do d.close() + set creating = false + set selectedId = project.id + this.renderList() + this.renderDetail() + + fun cancelCreate() = + let sub = subDialog("create") + if sub is ~null as d do + if d.open do d.close() + set creating = false + + fun requestRename(project) = + set pendingRename = project + let sub = subDialog("rename") + if sub is ~null as d do + let input = d.querySelector("input[name=\"project-rename\"]") + if input is ~null as field do + set field.value = project.name + window.setTimeout( + () => + field.focus() + field.select(), + 0 + ) + if not d.open do d.showModal() + + fun confirmRename() = + if pendingRename is + Absent then () + ~Absent as project then + let + sub = subDialog("rename") + input = if sub is Absent then null else sub.querySelector("input[name=\"project-rename\"]") + raw = if input is Absent then "" else input.value + name = raw.trim() + if name is "" then () + else + let didRename = projects.renameProject(project.id, name) + if sub is ~null as d do + if d.open do d.close() + set pendingRename = null + if didRename do + if projects.isCurrent(project.id) do + let detail = mut + id: projects.currentId() + project: projects.currentProject() + document.dispatchEvent(new CustomEvent("current-project-changed", mut { :detail })) + this.renderList() + this.renderDetail() + + fun cancelRename() = + set pendingRename = null + let sub = subDialog("rename") + if sub is ~null as d do + if d.open do d.close() + + fun requestDelete(project) = + set pendingDelete = project + let sub = subDialog("delete") + if sub is ~null as d do + let target = d.querySelector(".project-switcher-delete-target") + if target is ~null as box do + set box.innerHTML = """ + """ + escapeHtml(glyphFor(project)) + """ +
+
""" + escapeHtml(project.name) + """
+
""" + escapeHtml(filesLabel(totalFileCount(project.id))) + """
+
+ """ + if not d.open do d.showModal() + + fun confirmDelete() = + if pendingDelete is + Absent then () + ~Absent as project then + let didDelete = projects.deleteProject(project.id) + let sub = subDialog("delete") + if sub is ~null as d do + if d.open do d.close() + if didDelete do + let remaining = projects.listProjects() + if remaining.length > 0 do set selectedId = remaining.at(0).id + set pendingDelete = null + this.renderList() + this.renderDetail() + + fun cancelDelete() = + set pendingDelete = null + let sub = subDialog("delete") + if sub is ~null as d do + if d.open do d.close() + + fun attachListListeners(list) = + let panel = this + list.querySelectorAll("button.project-switcher-card").forEach of (button, ...) => + button.addEventListener of "click", () => + let id = button.dataset.projectId + if id is ~Absent do panel.selectProject(id) + button.addEventListener of "dblclick", () => + let id = button.dataset.projectId + if id is ~Absent do panel.openProject(id) + + fun attachDetailListeners(detail, project) = + let panel = this + if detail.querySelector("button.project-switcher-open") is ~null as openBtn do + openBtn.addEventListener("click", () => panel.openProject(project.id)) + if detail.querySelector("button.project-switcher-rename") is ~null as renameBtn do + renameBtn.addEventListener("click", () => panel.requestRename(project)) + if detail.querySelector("button.project-switcher-export") is ~null as exportBtn do + exportBtn.addEventListener("click", () => panel.handleExport(project)) + if detail.querySelector("button.project-switcher-delete") is ~null as deleteBtn do + deleteBtn.addEventListener("click", () => panel.requestDelete(project)) + + fun sanitizeForFilename(name) = + let raw = if name is Absent then "project" else name + let sanitized = raw.replace(new mut RegExp("[^a-z0-9]+", "gi"), "-").toLowerCase() + let trimmed = sanitized.replace(new mut RegExp("^-+|-+$", "g"), "") + if trimmed === "" then "project" else trimmed + + fun manifestContent(project) = + let payload = mut + schema: EXPORT_SCHEMA + exportedAt: new Date().toISOString() + project: project + JSON.stringify(payload, null, 2) + + // The Share dialog emits the in-memory tree as raw text; the project + // switcher emits localStorage snapshots, where each entry is a + // `{ content, readonly, atime, mtime, ctime, birthtime, attrs }` payload. + // For the archive we only want the text content at the original path. + fun handleExport(project) = + let + files = persistent.snapshotProject(project.id) + entries = mut [mut { path: EXPORT_MANIFEST_PATH, content: manifestContent(project) }] + files.forEach of (entry, ...) => + entries.push(mut { path: entry.path, content: entry.payload.content }) + let + bytes = Zip.build(entries) + filename = sanitizeForFilename(project.name) + ".zip" + Zip.download(bytes, filename, "application/zip") + Logger.info("Projects", "Exported project", project.id, "files:", files.length) + + fun showImportError(message) = + set importError = message + if this.querySelector(".project-switcher-import-error") is ~null as el do + if message === "" then + set + el.hidden = true + el.textContent = "" + else + set + el.hidden = false + el.textContent = message + + fun handleImportClick() = + showImportError("") + let sub = subDialog("import") + if sub is ~null as d do + if not d.open do d.showModal() + + fun chooseImportFile() = + let sub = subDialog("import") + if sub is ~null as d do + if d.querySelector(".project-switcher-import-input") is ~null as input do + set input.value = "" + input.click() + + fun closeImportDialog() = + let sub = subDialog("import") + if sub is ~null as d do + if d.open do d.close() + + // Restore the persisted payload shape so the imported project plays back + // through `restoreFile` on the next switch the same way an in-IDE-created + // file does. Timestamps default to "now" so the freshly-imported tree has + // sensible mtimes; the manifest's modifiedAt fills in the project meta. + fun filePayloadFor(content) = + let now = new Date().toISOString() + mut + content: content + readonly: false + atime: now + mtime: now + ctime: now + birthtime: now + attrs: mut {} + + fun isManifestEntry(entry) = + entry.path === EXPORT_MANIFEST_PATH + + fun absolutePath(zipPath) = + if zipPath.startsWith("/") then zipPath else "/" + zipPath + + fun importEntries(entries) = + let + manifest = null + fileEntries = mut [] + entries.forEach of (entry, ...) => + if isManifestEntry(entry) then + js.try_catch( + () => + let parsed = JSON.parse(entry.content) + set manifest = parsed + error => showImportError("manifest.json is not valid JSON: " + error.message) + ) + else fileEntries.push(entry) + if manifest is + null then showImportError("Archive is missing manifest.json") + else + let + schema = if manifest.schema is ~Absent then manifest.schema else "" + meta = if manifest.project is ~Absent then manifest.project else null + if + schema !== EXPORT_SCHEMA then showImportError("Not a v2 MLscript project export (schema: " + schema + ")") + meta is null then showImportError("manifest.json is missing project metadata") + else + let project = projects.adoptProject(meta) + fileEntries.forEach of (entry, ...) => + persistent.writeProjectFile(project.id, absolutePath(entry.path), filePayloadFor(entry.content)) + Logger.info("Projects", "Imported project", project.id, project.name, "files:", fileEntries.length) + set selectedId = project.id + set previewPath = "" + closeImportDialog() + this.renderList() + this.renderDetail() + + fun consumeImportBuffer(buffer) = + let panel = this + js.try_catch( + () => + let + bytes = Zip.fromArrayBuffer(buffer) + entries = Zip.parse(bytes) + panel.importEntries(entries) + error => panel.showImportError("Could not read ZIP: " + error.message) + ) + + fun handleImportFile(file) = + showImportError("") + let + panel = this + reader = new! window.FileReader() + set reader.onload = () => panel.consumeImportBuffer(reader.result) + set reader.onerror = () => panel.showImportError("Could not read file") + reader.readAsArrayBuffer(file) + + fun handleHostKeydown(event) = + if event.key === "Escape" do + if pendingDelete is ~Absent do cancelDelete() + if pendingRename is ~Absent do cancelRename() + if creating do cancelCreate() + + fun matchesOKey(event) = if + event.key === "o" then true + event.key === "O" then true + else false + + fun isOpenShortcut(event) = + let isMac = window.navigator.platform.toUpperCase().indexOf("MAC") >= 0 + let isCmdOrCtrl = if isMac then event.metaKey else event.ctrlKey + isCmdOrCtrl and matchesOKey(event) + + fun attachEventListeners() = + let panel = this + if dialog() is ~null as d do DialogHelpers.closeDismissibleDialogFromBackdropWithTransition(d) + if subDialog("create") is ~null as sub do closeOnBackdropClick(sub) + if subDialog("rename") is ~null as sub do closeOnBackdropClick(sub) + if subDialog("import") is ~null as sub do closeOnBackdropClick(sub) + if subDialog("delete") is ~null as sub do closeOnBackdropClick(sub) + document.addEventListener("workspace-switcher-requested", () => panel.openDialog()) + document.addEventListener of "keydown", event => + if panel.isOpenShortcut(event) do + event.preventDefault() + panel.openDialog() + if event.key === "Escape" do panel.handleHostKeydown(event) + if this.querySelector(".project-switcher-close") is ~null as closeBtn do + closeBtn.addEventListener("click", () => panel.close()) + if this.querySelector(".project-switcher-new") is ~null as newBtn do + newBtn.addEventListener("click", () => panel.handleCreate()) + if this.querySelector(".project-switcher-import") is ~null as importBtn do + importBtn.addEventListener("click", () => panel.handleImportClick()) + if subDialog("import") is ~null as sub do + if sub.querySelector(".project-switcher-cancel") is ~null as cancelBtn do + cancelBtn.addEventListener("click", () => panel.closeImportDialog()) + if sub.querySelector(".project-switcher-choose-file") is ~null as chooseBtn do + chooseBtn.addEventListener("click", () => panel.chooseImportFile()) + if sub.querySelector(".project-switcher-import-input") is ~null as input do + input.addEventListener of "change", event => + let files = input.files + if files is ~Absent and files.length > 0 do + panel.handleImportFile(files.at(0)) + set input.value = "" + if subDialog("create") is ~null as sub do + if sub.querySelector(".project-switcher-cancel") is ~null as cancelBtn do + cancelBtn.addEventListener("click", () => panel.cancelCreate()) + if sub.querySelector(".project-switcher-confirm-create") is ~null as confirmBtn do + confirmBtn.addEventListener("click", () => panel.confirmCreate()) + if sub.querySelector("input[name=\"project-name\"]") is ~null as input do + input.addEventListener of "keydown", event => + if event.key === "Enter" do + event.preventDefault() + panel.confirmCreate() + if subDialog("rename") is ~null as sub do + if sub.querySelector(".project-switcher-cancel") is ~null as cancelBtn do + cancelBtn.addEventListener("click", () => panel.cancelRename()) + if sub.querySelector(".project-switcher-confirm-rename") is ~null as confirmBtn do + confirmBtn.addEventListener("click", () => panel.confirmRename()) + if sub.querySelector("input[name=\"project-rename\"]") is ~null as input do + input.addEventListener of "keydown", event => + if event.key === "Enter" do + event.preventDefault() + panel.confirmRename() + if subDialog("delete") is ~null as sub do + if sub.querySelector(".project-switcher-cancel") is ~null as cancelBtn do + cancelBtn.addEventListener("click", () => panel.cancelDelete()) + if sub.querySelector(".project-switcher-confirm-delete") is ~null as confirmBtn do + confirmBtn.addEventListener("click", () => panel.confirmDelete()) + +customElements.define("project-switcher", ProjectSwitcher) diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/components/ResizeHandle.mls b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/ResizeHandle.mls new file mode 100644 index 0000000000..22d90ab009 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/ResizeHandle.mls @@ -0,0 +1,148 @@ +import "../common/JS.mls" + +open JS { Absent } + +class ResizeHandle extends HTMLElement with + let + isResizing = false + startX = 0 + startY = 0 + startSize = 0 + documentMouseMove = null + documentMouseUp = null + + fun connectedCallback() = + this.render() + this.attachEventListeners() + + fun direction() = if this.getAttribute("direction") is + Absent then "horizontal" + value then value + + fun side(defaultSide) = if this.getAttribute("side") is + Absent then defaultSide + value then value + + fun intAttribute(name, fallback) = if this.getAttribute(name) is + Absent then fallback + value then + let parsed = parseInt(value, 10) + if isNaN(parsed) then fallback else parsed + + fun isNaN(value) = + value !== value + + fun render() = + let dir = this.direction() + this.classList.remove("resize-handle-horizontal") + this.classList.remove("resize-handle-vertical") + this.classList.add("resize-handle") + this.classList.add("resize-handle-" + dir) + set this.innerHTML = """
""" + + fun attachEventListeners() = + this.addEventListener("mousedown", event => this.startResize(event)) + + fun targetElement() = if this.getAttribute("target") is + Absent then this.parentElement + target and target !== "" then document.querySelector(target) + else this.parentElement + + fun startResize(event) = + event.preventDefault() + let + dir = this.direction() + target = this.targetElement() + if target is ~Absent do + set + isResizing = true + startX = event.clientX + startY = event.clientY + startSize = if dir is "horizontal" then target.offsetWidth else target.offsetHeight + document.body.style.cursor = if dir is "horizontal" then "ew-resize" else "ns-resize" + document.body.style.userSelect = "none" + this.classList.add("active") + set + documentMouseMove = event => this.handleResize(event, target, dir) + documentMouseUp = () => this.stopResize(target) + document.addEventListener("mousemove", documentMouseMove) + document.addEventListener("mouseup", documentMouseUp) + + fun clamp(value, minSize, maxSize) = + Math.max(minSize, Math.min(maxSize, value)) + + fun setHorizontalSize(target, width) = + let container = document.querySelector(".app-container") + if + target.classList.contains("left-sidebar-panel") then + container.style.setProperty("--file-explorer-width", width + "px") + target.setAttribute("width", width) + target.classList.contains("right-sidebar-panel") then + container.style.setProperty("--ide-right-panel-width", width + "px") + target.setAttribute("width", width) + else () + + fun setVerticalSize(target, height) = + set + target.style.height = height + "px" + target.style.minHeight = height + "px" + target.style.maxHeight = height + "px" + target.setAttribute("height", height) + + fun resizeDetail(target) = + mut { width: target.offsetWidth, height: target.offsetHeight } + + fun resizeEventOptions(target) = + mut { detail: resizeDetail(target) } + + fun dispatchPanelResize(target) = + target.dispatchEvent(new CustomEvent("panel-resize", resizeEventOptions(target))) + + fun handleResize(event, target, dir) = + if isResizing do + let + minSize = this.intAttribute("min-size", 150) + maxSize = this.intAttribute("max-size", 600) + if dir is + "horizontal" then + let + deltaX = event.clientX - startX + newWidth = if + this.side("left") is "left" then startSize + deltaX + else startSize - deltaX + setHorizontalSize(target, clamp(newWidth, minSize, maxSize)) + else + let + deltaY = event.clientY - startY + newHeight = if + this.side("top") is "top" then startSize - deltaY + else startSize + deltaY + setVerticalSize(target, clamp(newHeight, minSize, maxSize)) + dispatchPanelResize(target) + + fun stopResize(target) = + set + isResizing = false + document.body.style.cursor = "" + document.body.style.userSelect = "" + this.classList.remove("active") + if documentMouseMove is ~Absent do + document.removeEventListener("mousemove", documentMouseMove) + if documentMouseUp is ~Absent do + document.removeEventListener("mouseup", documentMouseUp) + set + documentMouseMove = null + documentMouseUp = null + this.saveSize(target) + + fun saveSize(target) = + if target is ~Absent and target.saveSizeToStorage is ~Absent do + if + this.direction() === "horizontal" then + let width = parseInt(target.getAttribute("width"), 10) + if not isNaN(width) do target.saveSizeToStorage(width) + else + let height = parseInt(target.getAttribute("height"), 10) + if not isNaN(height) do target.saveSizeToStorage(height) + +customElements.define("resize-handle", ResizeHandle) diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/components/SearchPanel.mls b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/SearchPanel.mls new file mode 100644 index 0000000000..ebcd5993c1 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/SearchPanel.mls @@ -0,0 +1,259 @@ +import "../filesystem/fs.mls" +import "../common/JS.mls" + +open JS { Absent } + +fun escapeHtml(text) = + let div = document.createElement("div") + set div.textContent = text + div.innerHTML + +class SearchPanel extends HTMLElement with + let + query = "" + unsubscribe = null + collapsedGroups = new Set() + + fun connectedCallback() = + this.render() + this.attachEventListeners() + let panel = this + set unsubscribe = fs.subscribe(event => panel.handleFsEvent(event), undefined) + + fun disconnectedCallback() = + if unsubscribe is ~Absent as removeSubscription do removeSubscription() + + fun render() = + set this.innerHTML = """ +
+
+

Search

+
+
+ + +
+
+
+ """ + renderResults() + + fun normalizedQuery() = + query.trim().toLowerCase() + + fun fileName(path) = + path.split("/").pop() + + fun isSearchableFile(path, content) = + if + typeof(content) !== "string" then false + path.endsWith(".mls") then true + path.endsWith(".mjs") then true + path.endsWith(".js") then true + path.endsWith(".json") then true + path.endsWith(".md") then true + path.endsWith(".txt") then true + path.endsWith(".css") then true + path.endsWith(".html") then true + else false + + fun snippetForLine(line) = + if line.length > 160 then line.slice(0, 157) + "…" else line + + fun snippetForOccurrence(line, lowerLine, needle, matchIndex, needleLength) = + let + contextBefore = 12 + contextAfter = 24 + previousIndex = lowerLine.lastIndexOf(needle, matchIndex - 1) + nextIndex = lowerLine.indexOf(needle, matchIndex + needleLength) + defaultStart = Math.max(0, matchIndex - contextBefore) + defaultEnd = Math.min(line.length, matchIndex + needleLength + contextAfter) + start = if previousIndex >= defaultStart then previousIndex + needleLength else defaultStart + end = if nextIndex >= 0 and nextIndex < defaultEnd then nextIndex else defaultEnd + prefix = if start > 0 then "…" else "" + suffix = if end < line.length then "…" else "" + text = prefix + line.slice(start, end) + suffix + highlightStart = prefix.length + matchIndex - start + mut + text: text + highlightStart: highlightStart + + fun searchResult(path, line, excerpt, matchKind, matchStart, matchLength, highlightStart) = + mut + file: path + line: line + title: fileName(path) + excerpt: excerpt + matchKind: matchKind + matchStart: matchStart + matchLength: matchLength + highlightStart: highlightStart + + fun resultGroup(path) = + mut + file: path + title: fileName(path) + results: mut [] + + fun firstNonEmptyLine(lines) = + let result = "" + lines.forEach of (line, ...) => + if result === "" and line.trim() !== "" do set result = line.trim() + if result === "" then "(empty file)" else result + + fun contentMatches(path, lines, needle, results) = + lines.forEach of (line, index) => + let + lowerLine = line.toLowerCase() + needleLength = needle.length + matchIndex = lowerLine.indexOf(needle) + while matchIndex >= 0 and results.length < 100 do + let snippet = snippetForOccurrence(line, lowerLine, needle, matchIndex, needleLength) + results.push(searchResult(path, index + 1, snippet.text, "content", matchIndex, needleLength, snippet.highlightStart)) + set matchIndex = lowerLine.indexOf(needle, matchIndex + needleLength) + + fun pathMatches(path, lines, needle, results) = + if path.toLowerCase().includes(needle) and results.length < 100 do + results.push(searchResult(path, 1, snippetForLine(firstNonEmptyLine(lines)), "path", 0, 0, -1)) + + fun collectResults(needle) = + let + files = fs.getAllFiles(undefined) + results = mut [] + Object.keys(files).sort().forEach of (path, ...) => + if isSearchableFile(path, files.(path)) and results.length < 100 do + let lines = files.(path).split("\n") + pathMatches(path, lines, needle, results) + contentMatches(path, lines, needle, results) + results + + fun groupResults(results) = + let + groups = mut [] + byPath = mut {} + results.forEach of (result, ...) => + let path = result.file + if byPath.(path) is undefined do + set byPath.(path) = resultGroup(path) + groups.push(byPath.(path)) + byPath.(path).results.push(result) + groups + + fun searchSummaryHtml(results, groups) = + let + resultLabel = if results.length is 1 then "result" else "results" + fileLabel = if groups.length is 1 then "file" else "files" + """ +
+ """ + results.length + " " + resultLabel + " in " + groups.length + " " + fileLabel + """ +
+ """ + + fun highlightOccurrence(text, start, length) = + if start < 0 then escapeHtml(text) + else if length <= 0 then escapeHtml(text) + else + escapeHtml(text.slice(0, start)) + + """""" + escapeHtml(text.slice(start, start + length)) + """""" + + escapeHtml(text.slice(start + length)) + + fun resultHtml(result, needle, isHidden) = + let hiddenAttribute = if isHidden then """ hidden""" else "" + """ + + """ + + fun groupHtml(group, needle) = + let + isCollapsed = collapsedGroups.has(group.file) + collapsedClass = if isCollapsed then " collapsed" else "" + expanded = if isCollapsed then "false" else "true" + rows = group.results.map(result => resultHtml(result, needle, isCollapsed)).join("") + """ +
+ +
+ """ + rows + """ +
+
+ """ + + fun renderResults() = + if this.querySelector(".search-results") is ~null as results do + let needle = normalizedQuery() + if needle === "" then + set results.innerHTML = """
Type to search the workspace
""" + else + let + matching = collectResults(needle) + count = matching.length + if count is 0 then + set results.innerHTML = """
No results
""" + else + let + groups = groupResults(matching) + summary = searchSummaryHtml(matching, groups) + groupedResults = groups.map(group => groupHtml(group, needle)).join("") + set results.innerHTML = summary + groupedResults + attachGroupListeners() + attachResultListeners() + + fun setQuery(nextQuery) = + set query = nextQuery + renderResults() + + fun clearQuery() = + set query = "" + if this.querySelector(".search-input") is ~null as input do set input.value = "" + renderResults() + + fun handleFsEvent(event) = + if query.trim() !== "" do renderResults() + + fun dispatchOpenResult(button) = + let + filePath = button.dataset.filePath + line = parseInt(button.dataset.line, 10) + column = parseInt(button.dataset.column, 10) + length = parseInt(button.dataset.length, 10) + detail = mut + filePath: filePath + line: line + column: column + length: length + options = mut { :detail } + document.dispatchEvent(new CustomEvent("open-file-at-location", options)) + + fun attachResultListeners() = + let panel = this + this.querySelectorAll(".search-result-row").forEach of (button, ...) => + button.addEventListener("click", () => panel.dispatchOpenResult(button)) + + fun toggleGroup(path) = + if collapsedGroups.has(path) then collapsedGroups.delete(path) + else collapsedGroups.add(path) + renderResults() + + fun attachGroupListeners() = + let panel = this + this.querySelectorAll(".search-group-heading").forEach of (button, ...) => + button.addEventListener("click", () => panel.toggleGroup(button.dataset.filePath)) + + fun attachEventListeners() = + let panel = this + if this.querySelector(".search-input") is ~null as input do + input.addEventListener("input", event => panel.setQuery(event.target.value)) + if this.querySelector(".search-clear-button") is ~null as clearButton do + clearButton.addEventListener("click", () => panel.clearQuery()) + +customElements.define("search-panel", SearchPanel) diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/components/SettingsDialog.mls b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/SettingsDialog.mls new file mode 100644 index 0000000000..7c5efbc734 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/SettingsDialog.mls @@ -0,0 +1,581 @@ +import "../common/SettingsStore.mls" +import "../common/DialogHelpers.mls" +import "../common/JS.mls" + +open JS { Absent } + +class SettingsDialog extends HTMLElement with + let activeTab = "appearance" + let defaultEditorFontFamily = "'Google Sans Code', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace" + let defaultEditorFontVariationNormal = "'wght' 400" + let defaultEditorFontVariationBold = "'wght' 700" + let defaultEditorFontVariationItalic = "'wght' 400" + let defaultEditorFontVariationBoldItalic = "'wght' 700" + let defaultEditorLineHeight = "1.45" + let defaultEditorFontLigatures = "normal" + let defaultEditorLetterSpacing = "0" + let defaultEditorRulers = "" + + fun connectedCallback() = + this.render() + this.attachEventListeners() + + fun tab(id, label) = + mut { :id, :label } + + fun tabs() = + [ + tab("appearance", "Appearance") + tab("editor", "Editor") + tab("workbench", "Workbench") + tab("compile-run", "Compile & Run") + tab("keyboard-shortcuts", "Keyboard Shortcuts") + tab("data", "Data") + tab("about", "About") + ] + + fun tabButtonHtml(tabInfo) = + let activeClass = if tabInfo.id === activeTab then " active" else "" + """ + + """ + + fun tabPanelHtml(tabInfo) = + let activeClass = if tabInfo.id === activeTab then " active" else "" + """ +
""" + tabPanelContent(tabInfo.id) + """
+ """ + + fun toggleRowHtml(label, key, defaultValue) = + """ + + """ + + fun selectId(key) = + "settings-select-" + key.replaceAll(".", "-") + + fun selectRowHtml(label, key, defaultValue, optionsHtml) = + let id = selectId(key) + """ +
+ """ + label + """ +
+ + +
+
+ """ + + fun inputRowHtml(label, key, defaultValue, inputType, inputClass, attrs) = + """ + + """ + + fun textInputRowHtml(label, key, defaultValue) = + inputRowHtml(label, key, defaultValue, "text", "settings-text-input", "") + + fun numberInputRowHtml(label, key, defaultValue, min, max) = + inputRowHtml(label, key, defaultValue, "number", "settings-number-input", "min=\"" + min + "\" max=\"" + max + "\" step=\"1\"") + + fun numberInputRowHtmlWithStep(label, key, defaultValue, min, max, step) = + inputRowHtml(label, key, defaultValue, "number", "settings-number-input", "min=\"" + min + "\" max=\"" + max + "\" step=\"" + step + "\"") + + fun buttonRowHtml(label, action, text) = + """ +
+ """ + label + """ + +
+ """ + + fun optionHtml(value, label) = + """""" + + fun problemsScopeOptionsHtml() = + [ + optionHtml("workspace", "Workspace") + optionHtml("current", "Current file") + ].join("") + + fun logsLevelOptionsHtml() = + [ + optionHtml("all", "All levels") + optionHtml("error", "Error") + optionHtml("warn", "Warn") + optionHtml("info", "Info") + optionHtml("debug", "Debug") + optionHtml("trace", "Trace") + ].join("") + + fun editorRowsHtml() = + numberInputRowHtml("Font size", "editor.fontSize", "14", "10", "24") + + textInputRowHtml("Font family", "editor.fontFamily", defaultEditorFontFamily) + + numberInputRowHtmlWithStep("Line height", "editor.lineHeight", defaultEditorLineHeight, "1", "3", "0.05") + + textInputRowHtml("Font ligatures", "editor.fontLigatures", defaultEditorFontLigatures) + + numberInputRowHtmlWithStep("Letter spacing", "editor.letterSpacing", defaultEditorLetterSpacing, "-2", "8", "0.1") + + textInputRowHtml("Rulers", "editor.rulers", defaultEditorRulers) + + variableFontFeaturesHtml() + + toggleRowHtml("Word wrap", "editor.wordWrap", "false") + + toggleRowHtml("Indentation guides", "editor.indentGuides", "true") + + toggleRowHtml("Show whitespace", "editor.showWhitespace", "false") + + fun variableFontFeaturesHtml() = + """ +
+ """ + toggleRowHtml("Variable font features", "editor.variableFontFeatures", "false") + """ +
+ """ + textInputRowHtml("Normal", "editor.fontVariation.normal", defaultEditorFontVariationNormal) + """ + """ + textInputRowHtml("Bold", "editor.fontVariation.bold", defaultEditorFontVariationBold) + """ + """ + textInputRowHtml("Italic", "editor.fontVariation.italic", defaultEditorFontVariationItalic) + """ + """ + textInputRowHtml("Bold italic", "editor.fontVariation.boldItalic", defaultEditorFontVariationBoldItalic) + """ +
+
+ """ + + fun appearanceRowsHtml() = + toggleRowHtml("Use dark editor theme", "appearance.editorDarkTheme", "false") + + fun workbenchRowsHtml() = + buttonRowHtml("Layout", "reset-layout", "Reset Layout to Default") + + toggleRowHtml("Show Problems panel on startup", "workbench.showProblemsOnStartup", "true") + + toggleRowHtml("Reopen last project on launch", "workbench.reopenLastProject", "true") + + fun compileRunRowsHtml() = + selectRowHtml("Default Problems scope", "problems.defaultScope", "workspace", problemsScopeOptionsHtml()) + + selectRowHtml("Default Logs level", "logs.defaultLevel", "all", logsLevelOptionsHtml()) + + toggleRowHtml("Auto-compile on save", "compile.autoCompileOnSave", "false") + + toggleRowHtml("Auto-run after compile", "compile.autoRunAfterCompile", "false") + + fun keyboardShortcutsRowsHtml() = + let primary = primaryShortcutModifier() + staticRowHtml("Command Palette", primary + "+K") + + staticRowHtml("Workspace Symbols", primary + "+T") + + staticRowHtml("Compile current file", primary + "+S") + + staticRowHtml("Execute current file", primary + "+E") + + staticRowHtml("Toggle Files panel", primary + "+B") + + staticRowHtml("Toggle Problems panel", primary + "+Shift+B") + + staticRowHtml("Increase editor font", primary + "+=") + + staticRowHtml("Decrease editor font", primary + "+-") + + staticRowHtml("Close current tab", "Ctrl+W") + + fun aboutRowsHtml() = + staticRowHtml("Version", "Development build") + + staticRowHtml("Project", """https://github.com/hkust-taco/mlscript""") + + fun dataRowsHtml() = + """
""" + + buttonRowHtml("Defaults", "reset-app-defaults", "Reset app to defaults") + + fun tabPanelContent(id) = if id is + "appearance" then appearanceRowsHtml() + "editor" then editorRowsHtml() + "workbench" then workbenchRowsHtml() + "compile-run" then compileRunRowsHtml() + "keyboard-shortcuts" then keyboardShortcutsRowsHtml() + "data" then dataRowsHtml() + "about" then aboutRowsHtml() + else "" + + fun staticRowHtml(label, value) = + """ +
+ """ + label + """ + """ + value + """ +
+ """ + + fun render() = + set this.innerHTML = """ + +
+
+

Settings

+ +
+
+
+ """ + tabs().map(tabInfo => tabButtonHtml(tabInfo)).join("") + """ +
+
+ """ + tabs().map(tabInfo => tabPanelHtml(tabInfo)).join("") + """ +
+
+
+
+ """ + + fun dialog() = + this.querySelector("dialog") + + fun openDialog() = + loadSettingsIntoDialog() + if activeTab === "data" do refreshDataOverview() + if dialog() is ~null as settingsDialog do + DialogHelpers.showDialogWithTransition(settingsDialog) + if this.querySelector(".settings-tab.active") is ~null as activeTabButton do + window.setTimeout(() => activeTabButton.focus(), 0) + + fun closeDialog() = + if dialog() is ~null as settingsDialog do + DialogHelpers.closeDialogWithTransition(settingsDialog) + + fun switchTab(id) = + set activeTab = id + this.querySelectorAll("[data-settings-tab]").forEach of (button, ...) => + button.classList.toggle("active", button.dataset.settingsTab === id) + this.querySelectorAll("[data-settings-tab-panel]").forEach of (panel, ...) => + panel.classList.toggle("active", panel.dataset.settingsTabPanel === id) + if id === "data" do refreshDataOverview() + + fun isMacPlatform() = + window.navigator.platform.toUpperCase().indexOf("MAC") >= 0 + + fun primaryShortcutModifier() = + if isMacPlatform() then "Cmd" else "Ctrl" + + fun settingDefaultValue(element) = + if element.dataset.settingDefault is ~Absent as fallback then fallback + else if element.classList.contains("settings-toggle") then "false" + else element.value + + fun isNaN(value) = + value !== value + + fun parsedNumber(value, fallback) = + let parsed = parseFloat(value) + if isNaN(parsed) then fallback else parsed + + fun normalizedInputValue(element) = + if element.classList.contains("settings-number-input") then + let + fallback = parsedNumber(settingDefaultValue(element), 14) + parsed = parsedNumber(element.value, fallback) + min = parsedNumber(element.min, parsed) + max = parsedNumber(element.max, parsed) + clamped = Math.max(min, Math.min(max, parsed)) + set element.value = "" + clamped + "" + clamped + else element.value + + fun applySettingElement(element) = + if element.dataset.settingKey is ~Absent as key do + let value = SettingsStore.getSetting(key, settingDefaultValue(element)) + if element.classList.contains("settings-toggle") then + setSwitchChecked(element, value === "true") + else if element.classList.contains("settings-select-root") then + setCustomSelectValue(element, value, false) + else set element.value = value + + fun loadSettingsIntoDialog() = + this.querySelectorAll("[data-setting-key]").forEach of (element, ...) => + applySettingElement(element) + updateCascades() + + fun updateCascades() = + this.querySelectorAll("[data-settings-cascade-key]").forEach of (cascade, ...) => + let toggle = cascade.querySelector(".settings-toggle") + let enabled = if toggle is ~null as toggleInput then toggleInput.checked else false + cascade.classList.toggle("settings-cascade-open", enabled) + + fun persistSettingElement(element) = + if element.dataset.settingKey is ~Absent as key do + let value = if element.classList.contains("settings-toggle") then + if element.checked then "true" else "false" + else if element.classList.contains("settings-select-root") then element.dataset.value + else if element.classList.contains("settings-input") then normalizedInputValue(element) + else element.value + if element.classList.contains("settings-toggle") then + setSwitchChecked(element, element.checked) + SettingsStore.setSetting(key, value) + updateCascades() + else if element.classList.contains("settings-select-root") then + SettingsStore.setSetting(key, value) + else if element.classList.contains("settings-input") do + SettingsStore.setSetting(key, value) + document.dispatchEvent(new CustomEvent("setting-changed", mut { detail: mut { :key, :value } })) + + fun setSwitchChecked(element, checked) = + set element.checked = checked + element.setAttribute("aria-checked", if checked then "true" else "false") + + fun selectOptions(root) = + Array.from(root.querySelectorAll(".settings-select-option")) + + fun optionForValue(root, value) = + let selected = null + selectOptions(root).forEach of (option, ...) => + if selected is null and option.dataset.optionValue === value do + set selected = option + selected + + fun selectedOrFirstOption(root, value) = + let selected = optionForValue(root, value) + if selected is ~null then selected + else root.querySelector(".settings-select-option") + + fun setCustomSelectValue(root, value, persist) = + let option = selectedOrFirstOption(root, value) + if option is ~Absent as selected do + set root.dataset.value = selected.dataset.optionValue + if root.querySelector(".settings-select-value") is ~null as valueEl do + set valueEl.textContent = selected.textContent + selectOptions(root).forEach of (current, ...) => + current.setAttribute("aria-selected", if current === selected then "true" else "false") + if persist do persistSettingElement(root) + + fun closeCustomSelect(root, restoreFocus) = + if root is ~Absent do + if root.querySelector(".settings-select-trigger") is ~null as trigger do + trigger.setAttribute("aria-expanded", "false") + if restoreFocus do trigger.focus() + if root.querySelector(".settings-select-content") is ~null as content do + set content.hidden = true + + fun closeOtherCustomSelects(activeRoot) = + this.querySelectorAll(".settings-select-root").forEach of (root, ...) => + if root !== activeRoot do closeCustomSelect(root, false) + + fun closeAllCustomSelects() = + closeOtherCustomSelects(null) + + fun openCustomSelect(root, focusSelected) = + closeOtherCustomSelects(root) + if root.querySelector(".settings-select-trigger") is ~null as trigger do + trigger.setAttribute("aria-expanded", "true") + if root.querySelector(".settings-select-content") is ~null as content do + set content.hidden = false + if focusSelected do + let selected = selectedOrFirstOption(root, root.dataset.value) + if selected is ~Absent as selectedOption do selectedOption.focus() + + fun toggleCustomSelect(root) = + if root.querySelector(".settings-select-trigger") is ~null as trigger do + if trigger.getAttribute("aria-expanded") === "true" then closeCustomSelect(root, false) + else openCustomSelect(root, false) + + fun optionIndex(options, option) = + let index = -1 + options.forEach of (current, i) => + if index < 0 and current === option do set index = i + index + + fun focusSelectOption(root, offset) = + let + options = selectOptions(root) + active = document.activeElement + currentIndex = optionIndex(options, active) + baseIndex = if currentIndex < 0 then 0 else currentIndex + nextIndex = Math.max(0, Math.min(options.length - 1, baseIndex + offset)) + if options.at(nextIndex) is ~Absent as nextOption do nextOption.focus() + + fun focusSelectEdge(root, edge) = + let options = selectOptions(root) + let index = if edge === "start" then 0 else options.length - 1 + if options.at(index) is ~Absent as option do option.focus() + + fun selectActiveOption(option) = + let root = option.closest(".settings-select-root") + if root is ~Absent as selectRoot do + setCustomSelectValue(selectRoot, option.dataset.optionValue, true) + closeCustomSelect(selectRoot, true) + + fun handleCustomSelectClick(event) = + let option = event.target.closest(".settings-select-option") + if option is ~Absent as selectedOption do + event.preventDefault() + selectActiveOption(selectedOption) + else + let trigger = event.target.closest(".settings-select-trigger") + if trigger is ~Absent as selectTrigger do + event.preventDefault() + if selectTrigger.closest(".settings-select-root") is ~Absent as root do + toggleCustomSelect(root) + else if event.target.closest(".settings-select-root") is Absent do + closeAllCustomSelects() + + fun handleCustomSelectKeydown(event) = + let trigger = event.target.closest(".settings-select-trigger") + let option = event.target.closest(".settings-select-option") + if trigger is ~Absent as selectTrigger do + if selectTrigger.closest(".settings-select-root") is ~Absent as root do + if event.key is + "Enter" | " " | "ArrowDown" then + event.preventDefault() + openCustomSelect(root, true) + "ArrowUp" then + event.preventDefault() + openCustomSelect(root, true) + focusSelectEdge(root, "end") + else () + else if option is ~Absent as selectedOption do + if selectedOption.closest(".settings-select-root") is ~Absent as root do + if event.key is + "ArrowDown" then + event.preventDefault() + focusSelectOption(root, 1) + "ArrowUp" then + event.preventDefault() + focusSelectOption(root, -1) + "Home" then + event.preventDefault() + focusSelectEdge(root, "start") + "End" then + event.preventDefault() + focusSelectEdge(root, "end") + "Enter" | " " then + event.preventDefault() + selectActiveOption(selectedOption) + "Escape" then + event.preventDefault() + closeCustomSelect(root, true) + else () + + fun handleExternalLinkClick(event) = + if event.target.closest(".settings-link[href]") is ~Absent as link do + event.preventDefault() + window.open(link.href, "_blank", "noopener,noreferrer") + + fun storageStatsForPrefix(prefix) = + let + stats = mut { count: 0, bytes: 0 } + index = 0 + while index < localStorage.length do + let key = localStorage.key(index) + if key is ~Absent as storageKey do + if storageKey.startsWith(prefix) do + let value = localStorage.getItem(storageKey) + set stats.count += 1 + set stats.bytes += storageKey.length + if value is Absent then 0 else value.length + set index += 1 + stats + + fun storageStatsForKeys(keys) = + let stats = mut { count: 0, bytes: 0 } + keys.forEach of (key, ...) => + if localStorage.getItem(key) is ~Absent as value do + set stats.count += 1 + set stats.bytes += key.length + value.length + stats + + fun allStorageStats() = + let + stats = mut { count: localStorage.length, bytes: 0 } + index = 0 + while index < localStorage.length do + let key = localStorage.key(index) + if key is ~Absent as storageKey do + let value = localStorage.getItem(storageKey) + set stats.bytes += storageKey.length + if value is Absent then 0 else value.length + set index += 1 + stats + + fun formatBytes(bytes) = + if bytes < 1024 then bytes + " B" + else if bytes < 1024 * 1024 then (bytes / 1024).toFixed(1) + " KB" + else (bytes / (1024 * 1024)).toFixed(1) + " MB" + + fun dataStatRowHtml(label, stats) = + staticRowHtml(label, stats.count + " items Β· " + formatBytes(stats.bytes)) + + fun dataOverviewRowsHtml() = + dataStatRowHtml("Projects", storageStatsForKeys(["mlscript.projects.v1", "mlscript.project.current.v1", "mlscript.projects.schema"])) + + dataStatRowHtml("Persisted files", storageStatsForPrefix("mlscript-fs:proj:")) + + dataStatRowHtml("File path indexes", storageStatsForPrefix("mlscript-fs-paths:proj:")) + + dataStatRowHtml("Settings", storageStatsForPrefix("settings.")) + + dataStatRowHtml("Layout state", storageStatsForKeys([ + "file-explorer-width" + "diagnostics-inspector-width" + "bottom-panel-height" + "bottom-panel-active-tab" + "diagnostics-inspector-mode" + "logs-level-filter" + "logs-source-filter" + ])) + + dataStatRowHtml("Total browser storage", allStorageStats()) + + fun refreshDataOverview() = + if this.querySelector("[data-settings-data-overview]") is ~null as overview do + set overview.innerHTML = dataOverviewRowsHtml() + + fun resetLayoutToDefault() = + [ + "file-explorer-width" + "diagnostics-inspector-width" + "bottom-panel-height" + "bottom-panel-active-tab" + "diagnostics-inspector-mode" + "logs-level-filter" + "logs-source-filter" + ].forEach of (key, ...) => localStorage.removeItem(key) + window.location.reload() + + fun removeSettingsKeys() = + let + keys = mut [] + index = 0 + while index < localStorage.length do + let key = localStorage.key(index) + if key is ~Absent as storageKey do + if storageKey.startsWith("settings.") do keys.push(storageKey) + set index += 1 + keys.forEach of (key, ...) => localStorage.removeItem(key) + + fun resetAppToDefaults() = + if window.confirm("Reset app settings and layout to defaults?") do + removeSettingsKeys() + [ + "file-explorer-width" + "diagnostics-inspector-width" + "bottom-panel-height" + "bottom-panel-active-tab" + "diagnostics-inspector-mode" + "logs-level-filter" + "logs-source-filter" + ].forEach of (key, ...) => localStorage.removeItem(key) + window.location.reload() + + fun handleSettingAction(action) = + if action is + "reset-layout" then resetLayoutToDefault() + "reset-app-defaults" then resetAppToDefaults() + else () + + fun handleSettingsChange(event) = + if event.target is ~Absent as target do + if target.matches(".settings-toggle[data-setting-key]") then persistSettingElement(target) + else if target.matches(".settings-input[data-setting-key]") do persistSettingElement(target) + + fun attachEventListeners() = + let panel = this + if dialog() is ~null as settingsDialog do DialogHelpers.closeDismissibleDialogFromBackdropWithTransition(settingsDialog) + document.addEventListener("settings-dialog-open-requested", () => panel.openDialog()) + this.addEventListener("click", event => panel.handleCustomSelectClick(event)) + this.addEventListener("click", event => panel.handleExternalLinkClick(event)) + this.addEventListener("keydown", event => panel.handleCustomSelectKeydown(event)) + this.addEventListener("change", event => panel.handleSettingsChange(event)) + if this.querySelector(".settings-dialog-close") is ~null as closeButton do + closeButton.addEventListener("click", () => panel.closeDialog()) + this.querySelectorAll("[data-settings-tab]").forEach of (button, ...) => + button.addEventListener("click", () => panel.switchTab(button.dataset.settingsTab)) + this.querySelectorAll("[data-setting-action]").forEach of (button, ...) => + button.addEventListener("click", () => panel.handleSettingAction(button.dataset.settingAction)) + +customElements.define("settings-dialog", SettingsDialog) diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/components/ToolbarPanel.mls b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/ToolbarPanel.mls new file mode 100644 index 0000000000..4ba69eb60e --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/ToolbarPanel.mls @@ -0,0 +1,325 @@ +import "../common/JS.mls" +import "../filesystem/projects.mls" +import "https://esm.sh/@floating-ui/dom" { computePosition, shift, offset } + +open JS { Absent } + +class ToolbarPanel extends HTMLElement with + let + status = "idle" + runningTime = null + startTime = null + isCompiling = false + hasActiveFile = false + isMlsActive = false + + fun connectedCallback() = + this.render() + this.attachEventListeners() + let panel = this + window.addEventListener of "execution-status-change", event => + panel.setStatus(event.detail.status, event.detail.runningTime) + document.addEventListener of "compilation-status-change", event => + panel.setCompilationStatus(event.detail.status) + document.addEventListener of "active-tab-changed", event => + panel.setActiveFileMeta(event.detail) + document.addEventListener of "current-project-changed", () => + panel.updateProjectPill() + + fun setStatus(nextStatus, nextRunningTime) = + set + status = nextStatus + runningTime = nextRunningTime + this.updateStatusIndicator() + + fun setTerminateDisplay(terminateBtn, display) = if terminateBtn is ~Absent do set terminateBtn.style.display = display + + fun setTooltipText(tooltip, text) = if tooltip is ~Absent do set tooltip.textContent = text + + fun runningTooltip() = + let startedAt = new Date(startTime).toLocaleTimeString() + "Execution started at " + startedAt + "." + + fun doneStatusText() = if runningTime is + Absent then "Done" + time then "Done (" + time + "ms)" + + fun doneTooltip() = if runningTime is + Absent then "Execution completed successfully." + time then "Execution completed successfully in " + time + "ms." + + fun setIdleStatus(statusLight, statusText, terminateBtn, tooltip) = + statusLight.classList.add("status-idle") + set statusText.textContent = "Not running" + setTerminateDisplay(terminateBtn, "none") + setTooltipText(tooltip, "No execution is currently running.") + + fun setRunningStatus(statusLight, statusText, terminateBtn, tooltip) = + statusLight.classList.add("status-running") + set statusText.textContent = "Running..." + setTerminateDisplay(terminateBtn, "inline-block") + setTooltipText(tooltip, runningTooltip()) + + fun setCompilingStatus(statusLight, statusText, terminateBtn, tooltip) = + statusLight.classList.add("status-running") + set statusText.textContent = "Compiling..." + setTerminateDisplay(terminateBtn, "none") + setTooltipText(tooltip, "Compilation is running.") + + fun setDoneStatus(statusLight, statusText, terminateBtn, tooltip) = + statusLight.classList.add("status-done") + set statusText.textContent = doneStatusText() + setTerminateDisplay(terminateBtn, "none") + setTooltipText(tooltip, doneTooltip()) + + fun setErrorStatus(statusLight, statusText, terminateBtn, tooltip) = + statusLight.classList.add("status-error") + set statusText.textContent = "Error" + setTerminateDisplay(terminateBtn, "none") + setTooltipText(tooltip, "Execution encountered an error.") + + fun setAbortedStatus(statusLight, statusText, terminateBtn, tooltip) = + statusLight.classList.add("status-aborted") + set statusText.textContent = "Aborted" + setTerminateDisplay(terminateBtn, "none") + setTooltipText(tooltip, "Execution was aborted by the user.") + + fun setFatalStatus(statusLight, statusText, terminateBtn, tooltip) = + statusLight.classList.add("status-fatal") + set statusText.textContent = "Fatal error" + setTerminateDisplay(terminateBtn, "none") + setTooltipText(tooltip, "A fatal error occurred during execution.") + + fun updateStatusIndicator() = + let + statusLight = this.querySelector(".status-light") + statusText = this.querySelector(".status-text") + terminateBtn = this.querySelector("#terminate") + tooltip = this.querySelector(".status-tooltip") + if + statusLight is Absent then () + statusText is Absent then () + else + set statusLight.className = "status-light" + if status is + "idle" then setIdleStatus(statusLight, statusText, terminateBtn, tooltip) + "compiling" then setCompilingStatus(statusLight, statusText, terminateBtn, tooltip) + "running" then setRunningStatus(statusLight, statusText, terminateBtn, tooltip) + "done" then setDoneStatus(statusLight, statusText, terminateBtn, tooltip) + "error" then setErrorStatus(statusLight, statusText, terminateBtn, tooltip) + "aborted" then setAbortedStatus(statusLight, statusText, terminateBtn, tooltip) + "fatal" then setFatalStatus(statusLight, statusText, terminateBtn, tooltip) + else () + + fun projectGlyph(project) = if + project is Absent then "Β·" + project.name is Absent then "Β·" + project.icon is ~Absent and project.icon !== "" then project.icon + else project.name.charAt(0).toUpperCase() + + fun projectLabel(project) = if + project is Absent then "No project" + project.name is Absent then "No project" + else project.name + + fun brandHtml() = + """ +
+ MLscript +
+ """ + + fun pillHtml() = + let project = projects.currentProject() + """ +
+ """ + brandHtml() + """ + +
+ """ + + fun updateProjectPill() = + if this.querySelector("#project-pill") is ~null as pill do + let project = projects.currentProject() + if pill.querySelector(".project-switcher-pill-glyph") is ~null as glyph do + set glyph.textContent = projectGlyph(project) + if pill.querySelector(".project-switcher-pill-name") is ~null as label do + set label.textContent = projectLabel(project) + + fun handleProjectPillClick() = + document.dispatchEvent(new CustomEvent("workspace-switcher-requested")) + + fun render() = + set this.innerHTML = pillHtml() + """ +
+
+ Not running +
+ +
+ + + + + + +
+ """ + + fun getActiveFilePath() = + let editorPanel = document.querySelector("editor-panel") + if + editorPanel is Absent then null + editorPanel.activeTabId is Absent then null + editorPanel.openTabs is Absent then null + else + let activeTab = editorPanel.openTabs.get(editorPanel.activeTabId) + if activeTab is + Absent then null + ~Absent as tab then tab.path + + fun fileEventDetail() = + mut { filePath: this.getActiveFilePath() } + + fun fileEventOptions() = + mut { bubbles: true, detail: fileEventDetail() } + + fun bubbleEventOptions() = + mut { bubbles: true } + + fun handleCompile() = + if canCompileActiveFile() do + this.dispatchEvent(new CustomEvent("compile-requested", fileEventOptions())) + + fun handleExecute() = + if canExecuteActiveFile() do + this.dispatchEvent(new CustomEvent("execute-requested", fileEventOptions())) + + fun handleTerminate() = + this.dispatchEvent(new CustomEvent("terminate-requested", bubbleEventOptions())) + + fun handleCommandPalette() = + document.dispatchEvent(new CustomEvent("command-palette-open-requested")) + + fun handleShare() = + document.dispatchEvent(new CustomEvent("share-dialog-open-requested")) + + fun handleSettings() = + document.dispatchEvent(new CustomEvent("settings-dialog-open-requested")) + + fun setCompilationStatus(nextStatus) = + set isCompiling = nextStatus === "running" + if nextStatus is + "running" then setStatus("compiling", null) + "fatal" then setStatus("fatal", null) + "error" then setStatus("error", null) + "done" then setStatus("done", null) + else () + this.updateCompileButton() + this.updateExecuteButton() + + fun setActiveFileMeta(meta) = + set hasActiveFile = if + meta is Absent then false + meta.path is Absent then false + else meta.path !== "" + set isMlsActive = if + meta is Absent then false + meta.path is Absent then false + else meta.path.endsWith(".mls") + this.updateCompileButton() + this.updateExecuteButton() + + fun canCompileActiveFile() = + isMlsActive + + fun canExecuteActiveFile() = + hasActiveFile + + fun updateCompileButton() = if this.querySelector("#compile") is + ~null as compileBtn do + if + isCompiling then + set compileBtn.disabled = true + compileBtn.classList.add("loading") + set compileBtn.innerHTML = """ + + Compiling... + """ + else + set compileBtn.disabled = not canCompileActiveFile() + compileBtn.classList.remove("loading") + compileBtn.classList.toggle("disabled", not canCompileActiveFile()) + set compileBtn.innerHTML = """ + + Compile + """ + + fun updateExecuteButton() = if this.querySelector("#execute") is + ~null as executeBtn do + set executeBtn.disabled = not canExecuteActiveFile() + executeBtn.classList.toggle("disabled", not canExecuteActiveFile()) + + fun showTooltip(statusContainer, statusTooltip) = + set statusTooltip.style.display = "block" + computePosition(statusContainer, statusTooltip, + placement: "bottom" + middleware: [shift(padding: 5), offset(4)] + ).then of position => + set + statusTooltip.style.top = position.y + "px" + statusTooltip.style.left = position.x + "px" + + fun hideTooltip(statusTooltip) = + set statusTooltip.style.display = "none" + + fun attachButtonListeners(panel) = + if panel.querySelector("#project-pill") is + ~null as projectPillBtn do projectPillBtn.addEventListener("click", () => panel.handleProjectPillClick()) + if panel.querySelector("#command-palette") is + ~null as commandPaletteBtn do commandPaletteBtn.addEventListener("click", () => panel.handleCommandPalette()) + if panel.querySelector("#share") is + ~null as shareBtn do shareBtn.addEventListener("click", () => panel.handleShare()) + if panel.querySelector("#settings") is + ~null as settingsBtn do settingsBtn.addEventListener("click", () => panel.handleSettings()) + if panel.querySelector("#compile") is + ~null as compileBtn do compileBtn.addEventListener("click", () => panel.handleCompile()) + if panel.querySelector("#execute") is + ~null as executeBtn do executeBtn.addEventListener("click", () => panel.handleExecute()) + if panel.querySelector("#terminate") is + ~null as terminateBtn do terminateBtn.addEventListener("click", () => panel.handleTerminate()) + + fun attachStatusTooltip(panel) = + let + statusContainer = panel.querySelector(".status-container") + statusTooltip = panel.querySelector(".status-tooltip") + if statusContainer is ~Absent and statusTooltip is ~Absent do + statusContainer.addEventListener("mouseenter", () => panel.showTooltip(statusContainer, statusTooltip)) + statusContainer.addEventListener("mouseleave", () => panel.hideTooltip(statusTooltip)) + statusContainer.addEventListener("focus", () => panel.showTooltip(statusContainer, statusTooltip)) + statusContainer.addEventListener("blur", () => panel.hideTooltip(statusTooltip)) + + fun attachEventListeners() = + let panel = this + attachButtonListeners(panel) + attachStatusTooltip(panel) + +customElements.define("toolbar-panel", ToolbarPanel) diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/components/TreeNode.mls b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/TreeNode.mls new file mode 100644 index 0000000000..656dc10e72 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/components/TreeNode.mls @@ -0,0 +1,377 @@ +import "../filesystem/fs.mls" +import "../common/JS.mls" +import "../common/Logger.mls" + +open JS { Absent } + +fun emptyElements() = + mut + fileItem: null + fileNameText: null + compiledDot: null + details: null + summary: null + childrenContainer: null + fileIcon: null + fileName: null + mjsButton: null + folderIcon: null + folderName: null + +fun parentPathOf(path) = + path.substring(0, path.lastIndexOf("/")) + +fun fileNameOf(path) = + path.substring(path.lastIndexOf("/") + 1) + +fun basenameWithoutExt(name) = + name.slice(0, -4) + +fun childrenOf(node) = if node.children is + Absent then [] + else node.children + +fun nodeAttrs(node) = if node.attrs is + Absent then null + else node.attrs + +fun attrIsTrue(node, name) = + let attrs = nodeAttrs(node) + if attrs is + Absent then false + else attrs.(name) === true + +class TreeNode extends HTMLElement with + let + node = null + path = "" + parentFolderNode = null + elements = emptyElements() + childTreeNodes = new Map() + unsubscribe = null + + fun setupNameScroll(container, textEl) = if + container is Absent then () + textEl is Absent then () + container.dataset.scrollInit is ~Absent then () + else + set container.dataset.scrollInit = "true" + let start = () => + let distance = textEl.scrollWidth - container.clientWidth + 16 + if distance > 0 do + let duration = Math.min(12, Math.max(4, distance / 40)) + textEl.style.setProperty("--scroll-distance", distance + "px") + textEl.style.setProperty("--scroll-duration", duration + "s") + textEl.classList.add("scrolling") + let stop = () => + textEl.classList.remove("scrolling") + textEl.style.removeProperty("--scroll-distance") + textEl.style.removeProperty("--scroll-duration") + container.addEventListener("mouseenter", start) + container.addEventListener("mouseleave", stop) + container.addEventListener("focus", start) + container.addEventListener("blur", stop) + + fun connectedCallback() = + let treeNode = this + set unsubscribe = fs.subscribe(event => treeNode.handleFsEvent(event), undefined) + this.render() + + fun disconnectedCallback() = + if unsubscribe is ~Absent as unsubscribeCallback do unsubscribeCallback() + childTreeNodes.clear() + + fun setData(nextNode, nextPath, nextParentFolderNode) = + set + node = nextNode + path = nextPath + parentFolderNode = nextParentFolderNode + if this.isConnected do this.render() + + fun currentPath() = + path + + fun currentNodeName() = if node is + Absent then "" + else node.name + + fun handleFsEvent(event) = if event.'type is + "create" | "delete" then handleStructuralEvent(event) + "rename" then handleRenameEvent(event) + "readonly" | "attr" then handleRefreshEvent(event) + else () + + fun handleStructuralEvent(event) = + updateFolderForChildEvent(event.path) + updateMjsButtonForSiblingEvent(event.path) + + fun handleRenameEvent(event) = + if event.path === path do this.updateName() + updateFolderForChildEvent(event.path) + + fun handleRefreshEvent(event) = + if event.path === path do this.render() + + fun updateFolderForChildEvent(eventPath) = if node is ~Absent as current and current.'type is "folder" do + let eventParentPath = parentPathOf(eventPath) + if eventParentPath === path do this.updateChildren() + + fun updateMjsButtonForSiblingEvent(eventPath) = if node is ~Absent as current and current.'type is "file" and current.name.endsWith(".mls") do + let + eventParentPath = parentPathOf(eventPath) + myParentPath = parentPathOf(path) + if eventParentPath === myParentPath and eventPath.endsWith(".mjs") do + let + eventBasename = basenameWithoutExt(fileNameOf(eventPath)) + myBasename = basenameWithoutExt(current.name) + if eventBasename === myBasename do this.render() + + fun updateName() = if + node is Absent then () + node.'type is + "file" and elements.fileItem is ~Absent then + set elements.fileItem.textContent = node.name + "folder" and elements.summary is ~Absent then + set elements.summary.textContent = node.name + "/" + else () + + fun childPath(child) = if path is "" then child.name else path + "/" + child.name + + fun updateChildren() = if + node is Absent then () + node.'type !== "folder" then () + elements.childrenContainer is Absent then () + else + let + filteredChildren = filterMjsFiles(childrenOf(node)) + newChildPaths = new Set() + filteredChildren.forEach of (child, ...) => + newChildPaths.add(childPath(child)) + Array.from(childTreeNodes.entries()).forEach of (entry, ...) => + let + existingPath = entry.at(0) + childElement = entry.at(1) + if not newChildPaths.has(existingPath) do + childElement.remove() + childTreeNodes.delete(existingPath) + filteredChildren.forEach of (child, index) => + let nextPath = childPath(child) + if + not childTreeNodes.has(nextPath) then + let newChildElement = document.createElement("tree-node") + newChildElement.setData(child, nextPath, node) + childTreeNodes.set(nextPath, newChildElement) + let nextChild = elements.childrenContainer.children.(index) + if + nextChild is ~Absent then elements.childrenContainer.insertBefore(newChildElement, nextChild) + else elements.childrenContainer.appendChild(newChildElement) + else + let childElement = childTreeNodes.get(nextPath) + childElement.setData(child, nextPath, node) + let currentPosition = Array.from(elements.childrenContainer.children).indexOf(childElement) + if currentPosition !== index do + let nextChild = elements.childrenContainer.children.(index) + if nextChild !== childElement do + elements.childrenContainer.insertBefore(childElement, nextChild) + + fun filterMjsFiles(children) = + let mlsFiles = new Set() + children.forEach of (child, ...) => + if child.'type is "file" and child.name.endsWith(".mls") do + mlsFiles.add(basenameWithoutExt(child.name)) + children.filter of (child, ...) => + if + child.'type is "file" and child.name.endsWith(".mjs") then + let basename = basenameWithoutExt(child.name) + not mlsFiles.has(basename) + else true + + fun hasMjsFile() = if + node is Absent then false + node.'type !== "file" then false + not node.name.endsWith(".mls") then false + else hasSiblingMjsFile(node.name) + + fun hasSiblingMjsFile(name) = + let + mjsFileName = basenameWithoutExt(name) + ".mjs" + children = parentChildren() + children.some of (child, ...) => + child.'type is "file" and child.name === mjsFileName + + fun parentChildren() = if + parentFolderNode is Absent then dynamicParentChildren() + Array.isArray(parentFolderNode) then parentFolderNode + parentFolderNode.'type === "folder" then childrenOf(parentFolderNode) + else [] + + fun dynamicParentChildren() = + let parentPath = parentPathOf(path) + if + parentPath === "" and + let rootArray = fs.stat("/") + Array.isArray(rootArray) then rootArray + parentPath === "" then [] + let parentNode = fs.stat(parentPath) + parentNode is Absent then [] + parentNode.'type !== "folder" then [] + else childrenOf(parentNode) + + fun updateOpenState(openFiles) = if openFiles is ~Absent as currentOpenFiles do + updateOwnOpenState(currentOpenFiles) + Array.from(childTreeNodes.values()).forEach of (child, ...) => + child.updateOpenState(currentOpenFiles) + + fun updateOwnOpenState(openFiles) = if + node is Absent then () + node.'type !== "file" then () + elements.fileItem is Absent then () + else elements.fileItem.classList.toggle("open-in-editor", openFiles.has(path)) + + fun getMjsPath() = if not this.hasMjsFile() then null + else + let + mjsFileName = basenameWithoutExt(node.name) + ".mjs" + pathParts = path.split("/") + set pathParts.(pathParts.length - 1) = mjsFileName + pathParts.join("/") + + fun getFileIcon(fileName) = + let ext = fileName.substring(fileName.lastIndexOf(".")) + if ext is + ".mls" | ".mjs" | ".js" then "file-code" + ".json" then "braces" + ".md" | ".markdown" then "file-text" + else "file" + + fun fileOpenDetail(openPath, fileName) = + mut { path: openPath, :fileName } + + fun fileOpenOptions(openPath, fileName) = + mut { detail: fileOpenDetail(openPath, fileName), bubbles: true } + + fun dispatchFileOpen(openPath, fileName) = + this.dispatchEvent(new CustomEvent("file-open", fileOpenOptions(openPath, fileName))) + + fun render() = if + node is Absent then () + node.'type is + "file" then renderFile(node) + "folder" then renderFolder(node) + else () + + fun ensureFileElements() = if elements.fileItem is Absent do + let treeNode = this + set + elements.fileItem = document.createElement("div") + elements.fileIcon = document.createElement("i") + elements.fileName = document.createElement("span") + elements.fileNameText = document.createElement("span") + elements.compiledDot = document.createElement("span") + elements.fileItem.className = "file-item" + elements.fileName.className = "file-name" + elements.fileNameText.className = "file-name-text" + elements.compiledDot.className = "compiled-dot" + elements.fileName.appendChild(elements.fileNameText) + setupNameScroll(elements.fileName, elements.fileNameText) + elements.fileName.addEventListener of "click", () => + treeNode.dispatchFileOpen(treeNode.currentPath(), treeNode.currentNodeName()) + elements.fileItem.appendChild(elements.fileIcon) + elements.fileItem.appendChild(elements.fileName) + elements.fileItem.appendChild(elements.compiledDot) + this.appendChild(elements.fileItem) + + fun renderFile(current) = + ensureFileElements() + if current.readonly then + set elements.fileIcon.className = "file-icon icon-file-lock" + else + let icon = getFileIcon(current.name) + set elements.fileIcon.className = "file-icon icon-" + icon + set + elements.fileNameText.textContent = current.name + elements.fileItem.dataset.path = path + elements.fileItem.dataset.name = current.name + updateCompiledDot(current) + updateMjsButton(current) + + fun updateCompiledDot(current) = + let + isMls = current.name.endsWith(".mls") + isStd = attrIsTrue(current, "std") + isCompiled = attrIsTrue(current, "compiled") + shouldHide = if isMls then isStd else true + needsCompile = if + shouldHide then false + isCompiled then false + else true + elements.compiledDot.classList.toggle("hidden", shouldHide) + elements.compiledDot.classList.toggle("needs-compile", needsCompile) + set elements.compiledDot.title = if + shouldHide then "" + isCompiled then "Compiled" + else "Needs compile" + + fun updateMjsButton(current) = if + current.name.endsWith(".mls") and hasMjsFile() then ensureMjsButton(current) + current.name.endsWith(".mls") then removeMjsButton() + else () + + fun ensureMjsButton(current) = if elements.mjsButton is Absent do + let treeNode = this + set + elements.mjsButton = document.createElement("button") + elements.mjsButton.className = "mjs-button" + elements.mjsButton.textContent = ".mjs" + elements.mjsButton.title = "Open compiled .mjs file" + elements.mjsButton.addEventListener of "click", event => + event.stopPropagation() + let mjsPath = treeNode.getMjsPath() + if mjsPath is ~Absent as mjsFilePath do + let basename = basenameWithoutExt(treeNode.currentNodeName()) + treeNode.dispatchFileOpen(mjsFilePath, basename + ".mjs") + elements.fileItem.appendChild(elements.mjsButton) + + fun removeMjsButton() = if elements.mjsButton is ~Absent as mjsButton do + mjsButton.remove() + set elements.mjsButton = null + + fun ensureFolderElements(current) = if elements.details is Absent do + let + treeNode = this + isCollapsed = attrIsTrue(current, "collapsed") + set + elements.details = document.createElement("details") + elements.summary = document.createElement("summary") + elements.folderIcon = document.createElement("i") + elements.folderName = document.createElement("span") + elements.childrenContainer = document.createElement("div") + elements.details.open = not isCollapsed + elements.summary.dataset.path = path + elements.summary.dataset.name = current.name + "/" + elements.folderIcon.className = if isCollapsed then "folder-icon icon-folder" else "folder-icon icon-folder-open" + elements.folderName.textContent = current.name + elements.childrenContainer.className = "folder-children" + elements.childrenContainer.style.paddingLeft = "12px" + Logger.debug("File Tree", "Folder attributes", path, current.attrs) + elements.summary.appendChild(elements.folderIcon) + elements.summary.appendChild(elements.folderName) + elements.details.appendChild(elements.summary) + elements.details.appendChild(elements.childrenContainer) + elements.details.addEventListener("toggle", () => treeNode.updateFolderIcon()) + this.appendChild(elements.details) + + fun updateFolderIcon() = if elements.details is ~Absent and elements.folderIcon is ~Absent do + set elements.folderIcon.className = if + elements.details.open then "folder-icon icon-folder-open" + else "folder-icon icon-folder" + + fun renderFolder(current) = + ensureFolderElements(current) + set + elements.folderName.textContent = current.name + elements.summary.dataset.path = path + elements.summary.dataset.name = current.name + updateChildren() + +customElements.define("tree-node", TreeNode) diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/editor/Highlight.mls b/hkmc2/shared/src/test/mlscript-packages/web-ide/editor/Highlight.mls new file mode 100644 index 0000000000..92464918bf --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/editor/Highlight.mls @@ -0,0 +1,203 @@ +import "../common/JS.mls" + +open JS { Absent } + +module Highlight with... + +let makeWordRegex(...words) = new RegExp("^(?:" + words.join("|") + ")$") + +let keywords = makeWordRegex of + "val", "class", "trait", "type", "pattern", "object" + "this", "super", "true", "false", "null", "undefined" + "forall", "declare", "where", "with", "fun", "let", "constructor" + +let moduleKeywords = makeWordRegex("module", "open", "import") + +let controlKeywords = makeWordRegex of + "if", "then", "else", "case", "while", "do", "throw", "return" + +let operatorKeywords = makeWordRegex of + "and", "or", "not", "set", "new", "new!" + "restricts", "extends", "in", "as", "is", "of" + +let modifiers = makeWordRegex("mut", "abstract", "data") + +let types = makeWordRegex of + "Int", "Bool", "String", "Unit", "Num", "Nothing", "Anything" + "Object", "Array", "Function", "Error", "List", "Option", "Some", "None" + +let digit = new RegExp("[\\d.]") + +let operator = new RegExp("[+\\-*/%<>=!&|^~\\.]") + +let identiferStart = new RegExp("[$\\w_]") + +let identiferPart = new RegExp("[$\\w\\d_]") + +let capitalized = new RegExp("^[A-Z]") + +let debugCounters = mut {} + +fun streamText(stream) = if stream.string is + Absent then "" + null then "" + text then text + +fun streamPos(stream) = if stream.pos is + Absent then -1 + null then -1 + pos then pos + +fun streamStart(stream) = if stream.start is + Absent then -1 + null then -1 + start then start + +fun streamEnded(value) = if value is + Absent then true + null then true + else false + +fun maxTokenIterations(stream) = + Math.max(8, streamText(stream).length + 2) + +fun parseName(state) = if + state.parse is Absent then "unknown" + state.parse.name is Absent then "anonymous" + else state.parse.name + +fun trace(kind, stream, state, detail) = + let count = if debugCounters.(kind) is Absent then 0 else debugCounters.(kind) + set debugCounters.(kind) = count + 1 + if count < 40 do + console.warn("[MLscript Highlight] " + kind, mut + count: count + 1 + pos: streamPos(stream) + start: streamStart(stream) + lineLength: streamText(stream).length + line: streamText(stream) + parse: parseName(state) + detail: detail + ) + +fun normal(stream, state) = if stream.eol() then null else + let ch = stream.next() + if + streamEnded(ch) then null + ch is + "/" and + stream.eat("/") is Str then + stream.skipToEnd() + "comment" + stream.eat("*") is Str then + set state.parse = blockComment + state.parse(stream, state) + "\"" then + set state.parse = stringContent + state.parse(stream, state) + "=" and stream.eat(">") is Str then "operator" + digit.test(ch) then + stream.eatWhile(digit) + "number" + operator.test(ch) then + stream.eatWhile(operator) + "operator" + identiferStart.test(ch) and + do stream.eatWhile(identiferPart) + let word = stream.current() + keywords.test(word) then + set state.lastKeyword = word + "keyword" + moduleKeywords.test(word) then + set state.lastKeyword = null + "moduleKeyword" + controlKeywords.test(word) then + set state.lastKeyword = null + "controlKeyword" + operatorKeywords.test(word) then + set state.lastKeyword = null + "operatorKeyword" + modifiers.test(word) then + set state.lastKeyword = null + "modifier" + types.test(word) then + set state.lastKeyword = null + "typeName" + capitalized.test(word) then + set state.lastKeyword = null + "typeName" + state.lastKeyword is "fun" then + set state.lastKeyword = null + "propertyName" + else + set state.lastKeyword = null + "variableName" + else null + +fun blockComment(stream, state) = + let + shouldStop = false + iterations = 0 + maxIterations = maxTokenIterations(stream) + while not shouldStop do + set iterations += 1 + if iterations > maxIterations then + trace("block-comment-loop-guard", stream, state, mut { iterations: iterations }) + set shouldStop = true + else if stream.eol() then + set shouldStop = true + else + let ch = stream.next() + if + streamEnded(ch) then set shouldStop = true + ch is "*" and stream.eat("/") is Str then + set {state.parse = normal, shouldStop = true} + else set shouldStop = false + "comment" + +fun stringContent(stream, state) = + let + shouldStop = false + escaped = false + iterations = 0 + maxIterations = maxTokenIterations(stream) + while not shouldStop do + set iterations += 1 + if iterations > maxIterations then + trace("string-loop-guard", stream, state, mut { iterations: iterations, escaped: escaped }) + set shouldStop = true + else if stream.eol() then + set shouldStop = true + else + let ch = stream.next() + if + streamEnded(ch) then set shouldStop = true + ch is "\"" and not escaped then + set {state.parse = normal, shouldStop = true} + else + set escaped = not escaped and ch is "\\" + "string" + +fun startState() = mut { parse: normal, lastKeyword: null } + +fun token(stream, state) = + let before = streamPos(stream) + let style = if stream.eatSpace() then + null + else + state.parse(stream, state) + let after = streamPos(stream) + if after === before and not stream.eol() do + trace("token-no-progress", stream, state, mut { style: style }) + style + +val mlscript = + name: "mlscript" + startState: startState + token: token + languageData: + commentTokens: + line: "//" + block: + "open": "/*" + close: "*/" diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/editor/editor.mls b/hkmc2/shared/src/test/mlscript-packages/web-ide/editor/editor.mls new file mode 100644 index 0000000000..51cd4b58ec --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/editor/editor.mls @@ -0,0 +1,433 @@ +import "https://esm.sh/codemirror@6.0.1" { EditorView, basicSetup } +import "https://esm.sh/@codemirror/view@^6.0.0?target=es2022" { keymap, Decoration, ViewPlugin, highlightWhitespace } +import "https://esm.sh/@codemirror/state@^6.0.0?target=es2022" { EditorState, Compartment } +import "https://esm.sh/@codemirror/commands@^6.0.0?target=es2022" { indentWithTab } +import "https://esm.sh/@codemirror/lang-javascript@6.2.4" { javascript } +import "https://esm.sh/@codemirror/lang-markdown@6.3.4" { markdown } +import "https://esm.sh/@codemirror/language@6.11.3" { StreamLanguage, defaultHighlightStyle, syntaxHighlighting, foldService, indentUnit } +import "https://esm.sh/@uiw/codemirror-theme-vscode" { vscodeLight, vscodeDark } +import "./Highlight.mls" +import "../filesystem/fs.mls" +import "../common/SettingsStore.mls" +import "../common/JS.mls" + +open JS { Absent } + +module editor with... + +let editorFontSizeStorageKey = "editor.fontSize" +let editorFontFamilyStorageKey = "editor.fontFamily" +let editorFontVariationNormalStorageKey = "editor.fontVariation.normal" +let editorFontVariationBoldStorageKey = "editor.fontVariation.bold" +let editorFontVariationItalicStorageKey = "editor.fontVariation.italic" +let editorFontVariationBoldItalicStorageKey = "editor.fontVariation.boldItalic" +let editorVariableFontFeaturesStorageKey = "editor.variableFontFeatures" +let editorLineHeightStorageKey = "editor.lineHeight" +let editorFontLigaturesStorageKey = "editor.fontLigatures" +let editorLetterSpacingStorageKey = "editor.letterSpacing" +let editorRulersStorageKey = "editor.rulers" +let defaultEditorFontSize = 14 +let minEditorFontSize = 10 +let maxEditorFontSize = 24 +let defaultEditorFontFamily = "'Google Sans Code', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace" +let defaultEditorFontVariationNormal = "'wght' 400" +let defaultEditorFontVariationBold = "'wght' 700" +let defaultEditorFontVariationItalic = "'wght' 400" +let defaultEditorFontVariationBoldItalic = "'wght' 700" +let defaultEditorLineHeight = "1.45" +let defaultEditorFontLigatures = "normal" +let defaultEditorLetterSpacing = "0" +let defaultEditorRulers = "" + +let wordWrapCompartment = new! Compartment() +let indentGuidesCompartment = new! Compartment() +let whitespaceCompartment = new! Compartment() +let rulersCompartment = new! Compartment() +let editorViews = new Set() + +let foldStart = new RegExp("^\\s*(?:(?:data\\s+)?class|trait|object|module|fun|constructor)\\b") + +let nonBlank = new RegExp("\\S") + +fun isNaN(value) = + value !== value + +fun finiteNumber(value) = + typeof(value) is "number" and value === value + +fun numberOr(value, fallback) = + if finiteNumber(value) then value else fallback + +fun clampEditorFontSize(size) = + Math.max(minEditorFontSize, Math.min(maxEditorFontSize, size)) + +fun applyEditorFontSize(size) = + let clamped = clampEditorFontSize(size) + document.documentElement.style.setProperty("--editor-font-size", clamped + "px") + clamped + +fun parseCssNumber(value, fallback) = + let parsed = parseFloat(value) + if isNaN(parsed) then fallback else parsed + +fun cssSettingValue(value, fallback) = + if value is Absent then fallback + else if value.trim() === "" then fallback + else value + +fun applyEditorCssSetting(variableName, value, fallback) = + document.documentElement.style.setProperty(variableName, cssSettingValue(value, fallback)) + +fun applyEditorFontFamily(value) = + applyEditorCssSetting("--editor-font-family", value, defaultEditorFontFamily) + +fun applyEditorFontVariationNormal(value) = + applyEditorCssSetting("--editor-font-variation-normal", value, defaultEditorFontVariationNormal) + +fun applyEditorFontVariationBold(value) = + applyEditorCssSetting("--editor-font-variation-bold", value, defaultEditorFontVariationBold) + +fun applyEditorFontVariationItalic(value) = + applyEditorCssSetting("--editor-font-variation-italic", value, defaultEditorFontVariationItalic) + +fun applyEditorFontVariationBoldItalic(value) = + applyEditorCssSetting("--editor-font-variation-bold-italic", value, defaultEditorFontVariationBoldItalic) + +fun fontVariationValue(value, fallback) = + if settingEnabled(editorVariableFontFeaturesStorageKey, false) then cssSettingValue(value, fallback) + else "normal" + +fun restoreEditorFontVariations() = + applyEditorCssSetting("--editor-font-variation-normal", + fontVariationValue(SettingsStore.getSetting(editorFontVariationNormalStorageKey, defaultEditorFontVariationNormal), defaultEditorFontVariationNormal), + "normal") + applyEditorCssSetting("--editor-font-variation-bold", + fontVariationValue(SettingsStore.getSetting(editorFontVariationBoldStorageKey, defaultEditorFontVariationBold), defaultEditorFontVariationBold), + "normal") + applyEditorCssSetting("--editor-font-variation-italic", + fontVariationValue(SettingsStore.getSetting(editorFontVariationItalicStorageKey, defaultEditorFontVariationItalic), defaultEditorFontVariationItalic), + "normal") + applyEditorCssSetting("--editor-font-variation-bold-italic", + fontVariationValue(SettingsStore.getSetting(editorFontVariationBoldItalicStorageKey, defaultEditorFontVariationBoldItalic), defaultEditorFontVariationBoldItalic), + "normal") + +fun applyEditorLineHeight(value) = + let parsed = Math.max(1, Math.min(3, parseCssNumber(value, parseCssNumber(defaultEditorLineHeight, 1.45)))) + document.documentElement.style.setProperty("--editor-line-height", "" + parsed) + +fun applyEditorFontLigatures(value) = + applyEditorCssSetting("--editor-font-ligatures", value, defaultEditorFontLigatures) + +fun applyEditorLetterSpacing(value) = + let parsed = Math.max(-2, Math.min(8, parseCssNumber(value, parseCssNumber(defaultEditorLetterSpacing, 0)))) + document.documentElement.style.setProperty("--editor-letter-spacing", parsed + "px") + +fun rulerColumns(value) = + let columns = mut [] + cssSettingValue(value, defaultEditorRulers).split(",").forEach of (part, ...) => + let parsed = parseInt(part.trim(), 10) + if not isNaN(parsed) and parsed >= 0 do columns.push(parsed) + columns + +fun rulerGradient(column) = + "linear-gradient(to right, transparent " + column + "ch, var(--editor-ruler-color) " + column + "ch, var(--editor-ruler-color) calc(" + column + "ch + 1px), transparent calc(" + column + "ch + 1px))" + +fun applyEditorRulers(value) = + let columns = rulerColumns(value) + let background = if columns.length === 0 then "none" else columns.map(column => rulerGradient(column)).join(", ") + document.documentElement.style.setProperty("--editor-rulers-background", background) + +fun editorFontSizeFromSetting(value) = + let parsed = parseInt(value, 10) + if isNaN(parsed) then defaultEditorFontSize else parsed + +fun restoreEditorFontSize() = + applyEditorFontSize(editorFontSizeFromSetting(SettingsStore.getSetting(editorFontSizeStorageKey, "" + defaultEditorFontSize))) + +fun restoreEditorTypography() = + restoreEditorFontSize() + applyEditorFontFamily(SettingsStore.getSetting(editorFontFamilyStorageKey, defaultEditorFontFamily)) + restoreEditorFontVariations() + applyEditorLineHeight(SettingsStore.getSetting(editorLineHeightStorageKey, defaultEditorLineHeight)) + applyEditorFontLigatures(SettingsStore.getSetting(editorFontLigaturesStorageKey, defaultEditorFontLigatures)) + applyEditorLetterSpacing(SettingsStore.getSetting(editorLetterSpacingStorageKey, defaultEditorLetterSpacing)) + applyEditorRulers(SettingsStore.getSetting(editorRulersStorageKey, defaultEditorRulers)) + +fun currentEditorFontSize() = + let + rawValue = window.getComputedStyle(document.documentElement).getPropertyValue("--editor-font-size") + parsed = parseInt(rawValue, 10) + if isNaN(parsed) then defaultEditorFontSize else clampEditorFontSize(parsed) + +fun bumpEditorFontSize(delta) = + let nextSize = applyEditorFontSize(currentEditorFontSize() + delta) + SettingsStore.setSetting(editorFontSizeStorageKey, "" + nextSize) + nextSize + +fun settingEnabled(key, fallback) = + SettingsStore.getSetting(key, if fallback then "true" else "false") === "true" + +fun editorThemeExtension() = + if settingEnabled("appearance.editorDarkTheme", false) then vscodeDark else vscodeLight + +fun indentationColumns(text) = + let + columns = 0 + index = 0 + done = false + while not done and index < text.length do + let char = text.charAt(index) + if char is + " " then + set columns += 1 + set index += 1 + "\t" then + set columns += 2 + set index += 1 + else set done = true + columns + +fun foldableStructureLine(text) = + foldStart.test(text) + +fun foldRange(state, lineStart, lineEnd) = + let startLine = state.doc.lineAt(lineStart) + if not foldableStructureLine(startLine.text) then null + else + let + startIndent = indentationColumns(startLine.text) + lineNumber = startLine.number + 1 + lastContentLine = null + foundChildLine = false + done = false + while not done and lineNumber <= state.doc.lines do + let currentLine = state.doc.line(lineNumber) + if nonBlank.test(currentLine.text) then + let currentIndent = indentationColumns(currentLine.text) + if currentIndent <= startIndent then set done = true + else + set foundChildLine = true + set lastContentLine = currentLine + set lineNumber += 1 + else + if foundChildLine do set lastContentLine = currentLine + set lineNumber += 1 + if foundChildLine and lastContentLine !== null and lastContentLine.to > startLine.to then + mut { from: startLine.to, to: lastContentLine.to } + else null + +fun mlscriptFolding() = + foldService.of((state, lineStart, lineEnd) => foldRange(state, lineStart, lineEnd)) + +fun indentGuideColumns(columns) = + Math.floor(columns / 2) * 2 + +fun indentGuideStyle(columns) = + "--mls-indent-guide-width: " + indentGuideColumns(columns) + "ch" + +fun indentGuideLineDecoration(columns) = + let attributes = mut + style: indentGuideStyle(columns) + set attributes.'class = "mls-indent-guides" + Decoration.line(mut { :attributes }) + +fun buildIndentGuideDecorations(view) = + let ranges = mut [] + view.visibleRanges.forEach of (range, ...) => + let position = range.from + while position <= range.to do + let line = view.state.doc.lineAt(position) + let columns = indentationColumns(line.text) + if columns > 0 do + ranges.push(indentGuideLineDecoration(columns).range(line.from)) + set position = line.to + 1 + Decoration.set(ranges, true) + +fun indentGuidePluginValue(view) = + let plugin = mut + decorations: buildIndentGuideDecorations(view) + set plugin.update = update => + if update.docChanged then + set plugin.decorations = buildIndentGuideDecorations(update.view) + else if update.viewportChanged do + set plugin.decorations = buildIndentGuideDecorations(update.view) + plugin + +fun mlscriptIndentGuides() = + ViewPlugin.define(view => indentGuidePluginValue(view), mut + decorations: plugin => plugin.decorations + ) + +fun editorThemeSpec() = mut + "&": mut + height: "100%" + fontSize: "var(--editor-font-size)" + ".cm-scroller": mut + overflow: "auto" + ".cm-content": mut + caretColor: "var(--sand-12)" + fontFamily: "var(--editor-font-family)" + fontVariationSettings: "var(--editor-font-variation-normal)" + fontVariantLigatures: "var(--editor-font-ligatures)" + letterSpacing: "var(--editor-letter-spacing)" + lineHeight: "var(--editor-line-height)" + backgroundImage: "var(--editor-rulers-background)" + backgroundRepeat: "repeat-y" + ".cm-lineNumbers": mut + caretColor: "var(--sand-12)" + fontFamily: "var(--editor-font-family)" + fontVariationSettings: "var(--editor-font-variation-normal)" + ".cm-cursor": mut + borderLeftColor: "var(--sand-12)" + ".cm-gutters": mut + backgroundColor: "var(--sand-2)" + color: "var(--sand-11)" + borderRight: "none" + paddingLeft: "0" + ".cm-gutter": mut + borderRight: "none" + ".cm-lineNumbers .cm-gutterElement": mut + minWidth: "2.6ch" + paddingLeft: "14px" + paddingRight: "4px" + textAlign: "right" + ".cm-foldGutter .cm-gutterElement": mut + minWidth: "10px" + padding: "0" + ".cm-activeLineGutter": mut + backgroundColor: "var(--sand-2)" + +fun languageExtensions(extension) = + let extensions = mut [editorThemeExtension()] + if extension is + "mjs" | "js" then + extensions.push(javascript()) + "mls" then + extensions.push(StreamLanguage.define(Highlight.mlscript)) + extensions.push(mlscriptFolding()) + "md" | "markdown" then + extensions.push(markdown()) + else () + extensions + +fun readonlyExtension(isReadonly) = + if isReadonly then EditorView.editable.of(false) else [] + +fun saveOnChange(filePath, isReadonly) = + EditorView.updateListener.of of update => + if not isReadonly and update.docChanged do + let newContent = update.state.doc.toString() + fs.write(filePath, newContent) + if settingEnabled("compile.autoCompileOnSave", false) do + document.dispatchEvent(new CustomEvent("compile-requested", mut + bubbles: true + detail: mut { :filePath } + )) + +fun selectedLength(selection) = + let total = 0 + selection.ranges.forEach of (range, ...) => + set total += Math.abs(range.to - range.from) + total + +fun safeLineAt(state, head) = + state.doc.lineAt(Math.max(0, Math.min(state.doc.length, head))) + +fun cursorPositionDetail(state, filePath) = + let + head = numberOr(state.selection.main.head, 0) + line = safeLineAt(state, head) + lineFrom = numberOr(line.from, head) + column = Math.max(1, head - lineFrom + 1) + mut + :filePath + line: line.number + column: column + selected: selectedLength(state.selection) + +fun dispatchCursorPosition(state, filePath) = + document.dispatchEvent(new CustomEvent("cursor-position-changed", mut { detail: cursorPositionDetail(state, filePath) })) + +fun cursorPositionOnChange(filePath) = + EditorView.updateListener.of of update => + if update.selectionSet then dispatchCursorPosition(update.state, filePath) + else if update.docChanged do dispatchCursorPosition(update.state, filePath) + +fun configuredWordWrap() = + if settingEnabled("editor.wordWrap", false) then EditorView.lineWrapping else [] + +fun configuredIndentGuides() = + if settingEnabled("editor.indentGuides", true) then mlscriptIndentGuides() else [] + +fun configuredWhitespace() = + if settingEnabled("editor.showWhitespace", false) then highlightWhitespace() else [] + +fun configuredRulers() = + EditorView.theme(mut + ".cm-content": mut + backgroundImage: "var(--editor-rulers-background)" + backgroundRepeat: "repeat-y" + ) + +fun reconfigureAll(compartment, extension) = + editorViews.forEach of (view, ...) => + view.dispatch(mut { effects: compartment.reconfigure(extension) }) + +fun handleSettingChanged(detail) = + if detail is ~Absent as setting do + if setting.key === editorFontSizeStorageKey then applyEditorFontSize(editorFontSizeFromSetting(setting.value)) + else if setting.key === editorFontFamilyStorageKey then applyEditorFontFamily(setting.value) + else if setting.key === editorVariableFontFeaturesStorageKey then restoreEditorFontVariations() + else if setting.key === editorFontVariationNormalStorageKey then restoreEditorFontVariations() + else if setting.key === editorFontVariationBoldStorageKey then restoreEditorFontVariations() + else if setting.key === editorFontVariationItalicStorageKey then restoreEditorFontVariations() + else if setting.key === editorFontVariationBoldItalicStorageKey then restoreEditorFontVariations() + else if setting.key === editorLineHeightStorageKey then applyEditorLineHeight(setting.value) + else if setting.key === editorFontLigaturesStorageKey then applyEditorFontLigatures(setting.value) + else if setting.key === editorLetterSpacingStorageKey then applyEditorLetterSpacing(setting.value) + else if setting.key === editorRulersStorageKey then + applyEditorRulers(setting.value) + reconfigureAll(rulersCompartment, configuredRulers()) + else if setting.key === "editor.wordWrap" then reconfigureAll(wordWrapCompartment, configuredWordWrap()) + else if setting.key === "editor.indentGuides" then reconfigureAll(indentGuidesCompartment, configuredIndentGuides()) + else if setting.key === "editor.showWhitespace" do reconfigureAll(whitespaceCompartment, configuredWhitespace()) + +document.addEventListener("setting-changed", event => handleSettingChanged(event.detail)) +restoreEditorTypography() + +fun editorExtensions(filePath, extension, isReadonly) = + restoreEditorTypography() + let extensions = mut [basicSetup] + extensions.push(keymap.of([indentWithTab])) + extensions.push(indentUnit.of(" ")) + extensions.push(EditorState.tabSize.of(2)) + extensions.push(wordWrapCompartment.of(configuredWordWrap())) + extensions.push(indentGuidesCompartment.of(configuredIndentGuides())) + extensions.push(whitespaceCompartment.of(configuredWhitespace())) + extensions.push(rulersCompartment.of(configuredRulers())) + extensions.push(syntaxHighlighting(defaultHighlightStyle)) + languageExtensions(extension).forEach of (extension, ...) => + extensions.push(extension) + extensions.push(saveOnChange(filePath, isReadonly)) + extensions.push(cursorPositionOnChange(filePath)) + extensions.push(readonlyExtension(isReadonly)) + extensions.push(EditorView.theme(editorThemeSpec())) + extensions + +fun editorOptions(container, initialContent, filePath, extension, isReadonly) = + mut + doc: initialContent + extensions: editorExtensions(filePath, extension, isReadonly) + parent: container + +fun createEditor(container, initialContent, filePath, extension, isReadonly) = + let view = Reflect.construct of EditorView, [ + editorOptions(container, initialContent, filePath, extension, isReadonly) + ] + let destroy = view.destroy.bind(view) + set view.destroy = () => + editorViews.delete(view) + destroy() + editorViews.add(view) + dispatchCursorPosition(view.state, filePath) + view diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/execution/runner.mls b/hkmc2/shared/src/test/mlscript-packages/web-ide/execution/runner.mls new file mode 100644 index 0000000000..e7f00662e2 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/execution/runner.mls @@ -0,0 +1,235 @@ +import "../filesystem/fs.mls" +import "../common/JS.mls" +import "../common/Logger.mls" + +open JS { Absent } + +module runner with... + +let latestExecution = null + +fun workerOptions() = + mut { 'type: 'module } + +fun statusDetail(status, runningTime) = + mut { :status, :runningTime } + +fun customEventOptions(status, runningTime) = + mut { detail: statusDetail(status, runningTime), bubbles: true } + +fun dispatchStatusChange(status, runningTime) = + let event = new CustomEvent("execution-status-change", customEventOptions(status, runningTime)) + window.dispatchEvent(event) + +fun consolePanel() = + document.querySelector("bottom-panel") + +fun diagnosticsInspector() = + document.querySelector("diagnostics-inspector") + +fun clearConsolePanel() = + if consolePanel() is + ~null as panel do panel.clear() + +fun runMessage(id, mainPath, files) = + mut { 'type: 'run, :id, :mainPath, :files } + +fun workerErrorDetails(event) = + mut + message: event.message + filename: event.filename + lineno: event.lineno + colno: event.colno + error: event.error + fullEvent: event + +fun errorStack(error) = if + error is Absent then null + error.stack is Absent then null + else error.stack + +fun appendWorkerLocation(panel, event) = + if event.filename is ~Absent as filename do + panel.log("error", " at " + filename + ":" + event.lineno + ":" + event.colno) + +fun appendWorkerStack(panel, event) = + if errorStack(event.error) is + ~null as stack do panel.log("error", stack) + +fun workerErrorMessage(event) = + let message = "Worker Error:\n" + if event.message is ~Absent as messageText do set message += "Message: " + messageText + "\n" + if event.filename is ~Absent as filename do set message += "File: " + filename + "\n" + if event.lineno is ~Absent as lineno do set message += "Line: " + lineno + ":" + event.colno + "\n" + if errorStack(event.error) is + ~null as stack do set message += "\nStack:\n" + stack + if event.error is ~Absent as error and errorStack(error) is Absent do + set message += "\nStack:\n" + error + message + +fun payloadName(payload) = if payload.name is + Absent then "Error" + name then String(name) + +fun payloadMessage(payload) = if payload.message is + Absent then String(payload) + message then String(message) + +fun payloadStack(payload) = if payload.stack is + Absent then "" + stack then String(stack) + +fun executionErrorSummary(sourcePath, mainPath, payload) = + "Execution error in " + sourcePath + " while running " + mainPath + ": " + payloadMessage(payload) + +fun executionErrorDetail(sourcePath, mainPath, payload) = + let detail = payloadName(payload) + ": " + payloadMessage(payload) + "\n\nCulprit: " + sourcePath + "\nExecuted module: " + mainPath + if payloadStack(payload) is + "" then detail + stack then detail + "\n\nStack:\n" + stack + +fun executionDiagnostic(sourcePath, mainPath, payload) = + let detail = executionErrorDetail(sourcePath, mainPath, payload) + mut + kind: "error" + source: "execution" + mainMessage: executionErrorSummary(sourcePath, mainPath, payload) + excerpt: detail + line: 1 + allMessages: [ + mut + messageBits: [mut { text: detail }] + location: mut { start: 0, end: 0 } + ] + +fun setExecutionDiagnostics(sourcePath, mainPath, payload) = + if diagnosticsInspector() is + ~null as panel do panel.setDiagnostics([ + mut + path: sourcePath + diagnostics: [executionDiagnostic(sourcePath, mainPath, payload)] + ]) + +fun logExecutionErrorToOutput(sourcePath, mainPath, payload) = + if consolePanel() is + ~null as panel do panel.log("error", executionErrorSummary(sourcePath, mainPath, payload)) + +fun handleExecutionError(execution, sourcePath, mainPath, payload) = + let summary = executionErrorSummary(sourcePath, mainPath, payload) + Logger.error("Execution Worker", summary, payload) + set execution.isRunning = false + setExecutionDiagnostics(sourcePath, mainPath, payload) + logExecutionErrorToOutput(sourcePath, mainPath, payload) + dispatchStatusChange("error", null) + +fun makeExecution(id) = + mut + :id + worker: new Worker("execution/worker.mjs", workerOptions()) + isRunning: false + startTime: null + +fun run(execution, id, mainPath) = + let files = fs.getAllFiles(undefined) + Logger.debug("Execution", "Files sent to worker", Object.keys(files)) + execution.worker.postMessage(runMessage(id, mainPath, files)) + +fun isOutdated(id) = if latestExecution is + null then true + execution and execution.id !== id then true + else false + +fun handleVmConsole(kind, logMethod, payload) = + if consolePanel() is + ~null as panel do panel.log(kind, ...payload) + +fun handleWorkerMessage(execution, id, sourcePath, mainPath, event) = + if isOutdated(id) then + Logger.warn("Execution", "Received message from outdated worker, terminating it") + execution.worker.terminate() + else + let + messageData = event.data + kind = messageData.'type + payload = messageData.payload + if kind is + "ready" then + Logger.info("Execution", "Worker ready to run") + set + execution.isRunning = true + execution.startTime = Date.now() + dispatchStatusChange("running", null) + run(execution, id, mainPath) + "internal-log" then Logger.emit(payload.level, payload.source, payload.message, payload.body) + "log" then Logger.debug("Execution Worker", String(payload)) + "runtime-error" then handleExecutionError(execution, sourcePath, mainPath, payload) + "error" then + handleExecutionError(execution, sourcePath, mainPath, payload) + "done" then + Logger.info("Execution", "Execution finished", payload) + set execution.isRunning = false + let runningTime = if + execution.startTime is Absent then null + else Date.now() - execution.startTime + dispatchStatusChange("done", runningTime) + "console.log" then handleVmConsole("log", "log", payload) + "console.error" then handleVmConsole("error", "error", payload) + "console.warn" then handleVmConsole("warn", "warn", payload) + else () + +fun handleWorkerError(execution, event) = + Logger.error("Execution Worker", "Worker error event", workerErrorDetails(event)) + set execution.isRunning = false + dispatchStatusChange("fatal", null) + if consolePanel() is + ~null as panel do + let message = if + event.message is Absent then "Unknown error" + else event.message + panel.log("error", "Worker Error: " + message) + appendWorkerLocation(panel, event) + appendWorkerStack(panel, event) + if diagnosticsInspector() is + ~null as panel do panel.setOutput(workerErrorMessage(event)) + +fun handleMessageError(execution, event) = + Logger.error("Execution Worker", "Worker message error", event) + set execution.isRunning = false + dispatchStatusChange("fatal", null) + if consolePanel() is + ~null as panel do panel.log("error", "Worker Message Error: Failed to deserialize message from worker") + if diagnosticsInspector() is + ~null as panel do panel.setOutput("Worker Message Error: Failed to deserialize message from worker") + +fun cleanupPreviousExecution() = if latestExecution is + null then true + execution and + execution.isRunning then + Logger.warn("Execution", "The previous execution is still running. Stop it before starting a new one") + false + else + Logger.debug("Execution", "Clean up the previous execution") + execution.worker.terminate() + set latestExecution = null + true + +fun execute(mainPath, sourcePath) = if cleanupPreviousExecution() do + clearConsolePanel() + Logger.info("Execution", "Starting new execution", mainPath) + let + id = Date.now().toString() + execution = makeExecution(id) + set latestExecution = execution + set execution.worker.onmessage = event => handleWorkerMessage(execution, id, sourcePath, mainPath, event) + set execution.worker.onerror = event => handleWorkerError(execution, event) + execution.worker.addEventListener("messageerror", event => handleMessageError(execution, event)) + +fun terminate() = if latestExecution is + ~null as execution and execution.isRunning do + Logger.warn("Execution", "Terminating worker") + execution.worker.terminate() + set execution.isRunning = false + dispatchStatusChange("aborted", null) + if consolePanel() is + ~null as panel do panel.log("warn", "Execution terminated by user") + set latestExecution = null diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/execution/worker.mls b/hkmc2/shared/src/test/mlscript-packages/web-ide/execution/worker.mls new file mode 100644 index 0000000000..bb6a5ff81d --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/execution/worker.mls @@ -0,0 +1,193 @@ +import "../common/JS.mls" + +open JS { Absent } + +module worker with... + +let ModuleSource = null +let Compartment = null + +fun dynamicImport(specifier) = + let importModule = Function("specifier", "return import(specifier)") + importModule(specifier) + +fun message(kind, payload) = + mut { 'type: kind, :payload } + +fun post(kind, payload) = + self.postMessage(message(kind, payload)) + +fun workerLog(level, message, ...body) = + post("internal-log", mut { :level, source: "Execution Worker", :message, :body }) + +fun errorStack(error) = if + error is Absent then "" + error.stack is + Absent then String(error) + stack then stack + +fun errorMessage(error) = if + error is Absent then "Unknown error" + error.message is + Absent then String(error) + message then message + +fun errorName(error) = if + error is Absent then "Error" + error.name is + Absent then "Error" + name then name + +fun serializedError(error) = + mut + name: errorName(error) + message: errorMessage(error) + stack: errorStack(error) + +fun dependencyErrorPayload(error) = + "Failed to load worker dependencies: " + errorMessage(error) + "\n" + errorStack(error) + +fun requireDependency(value, dependencyName) = + if value is Absent do throw new Error(dependencyName + " not found after loading worker dependencies") + +fun finishDependencyLoad(endoModuleSource) = + set ModuleSource = endoModuleSource.ModuleSource + requireDependency(Compartment, "Compartment constructor") + requireDependency(ModuleSource, "ModuleSource") + post("ready", null) + +fun loadEndoModuleSource(sesModule) = + if self.Compartment is + Absent then set Compartment = sesModule.Compartment + compartment then set Compartment = compartment + let promise = dynamicImport("https://esm.sh/@endo/module-source@1.3.3") + promise.then(finishDependencyLoad) + +fun handleDependencyError(error) = + post("error", dependencyErrorPayload(error)) + throw error + +fun loadDependencies() = + let + promise = dynamicImport("https://esm.sh/ses@1.14.0") + handled = promise.then(loadEndoModuleSource) + handled.'catch(handleDependencyError) + +fun normalizePath(path) = + let + parts = mut [] + segments = path.split("/") + i = 0 + while i < segments.length do + let part = segments.at(i) + if part is + "" | "." then () + ".." then + if parts.length > 0 do parts.pop() + else parts.push(part) + set i += 1 + "/" + parts.join("/") + +fun resolveRelative(specifier, referrer) = + let + idx = referrer.lastIndexOf("/") + dir = if idx === -1 then "/" else referrer.slice(0, idx + 1) + normalizePath(dir + specifier) + +fun workerGlobalError(message, source, lineno, colno, error) = + let details = mut { :message, :source, :lineno, :colno, :error } + workerLog("error", "Worker global error", details) + post("error", "Uncaught error in worker: " + message + "\n" + errorStack(error)) + true + +fun unhandledRejection(event) = + workerLog("error", "Worker unhandled rejection", event.reason) + post("error", "Unhandled promise rejection: " + errorStack(event.reason)) + event.preventDefault() + +fun consolePayload(kind, args) = + post(kind, args) + +fun vmLog(...args) = + consolePayload("console.log", args) + +fun vmError(...args) = + consolePayload("console.error", args) + +fun vmWarn(...args) = + consolePayload("console.warn", args) + +fun vmConsole() = + mut { log: vmLog, error: vmError, warn: vmWarn } + +fun endowments() = + mut + console: vmConsole() + fetch: self.fetch + structuredClone: self.structuredClone + +fun resolveHook(moduleSpecifier, moduleReferrer) = + post("log", "Resolving module: " + moduleSpecifier + " from " + moduleReferrer) + if + moduleSpecifier.startsWith("./") then resolveRelative(moduleSpecifier, moduleReferrer) + moduleSpecifier.startsWith("../") then resolveRelative(moduleSpecifier, moduleReferrer) + moduleSpecifier.startsWith("/") then normalizePath(moduleSpecifier) + else moduleSpecifier + +fun importHook(fileMap, fullSpecifier) = + let + path = normalizePath(fullSpecifier) + src = fileMap.get(path) + if src is Absent then throw new Error("Module not found: " + path) + else + post("log", "Importing module: " + path) + Reflect.construct(ModuleSource, [src, path]) + +fun compartmentOptions(fileMap) = + mut + resolveHook: resolveHook + importHook: fullSpecifier => importHook(fileMap, fullSpecifier) + +fun makeCompartment(fileMap) = + let modules = mut {} + Reflect.construct of Compartment, [ + endowments() + modules + compartmentOptions(fileMap) + ] + +fun runDone(_) = + post("done", null) + +fun runError(error) = + post("runtime-error", serializedError(error)) + +fun runMain(mainPath, files) = + post("log", "Message received...") + let + fileMap = new Map(Object.entries(files)) + compartment = makeCompartment(fileMap) + post("log", "Importing the main module...") + let handled = compartment.import(normalizePath(mainPath)).then(runDone) + handled.'catch(runError) + +fun messageHandlerError(error) = + let msg = "Error in worker message handler: " + errorStack(error) + workerLog("error", msg) + post("error", msg) + +fun handleMessageData(messageData) = + if messageData.'type is "run" do runMain(messageData.mainPath, messageData.files) + +fun handleMessage(event) = + js.try_catch( + () => handleMessageData(event.data) + messageHandlerError + ) + +set + self.onerror = workerGlobalError + self.onunhandledrejection = unhandledRejection + self.onmessage = handleMessage + +loadDependencies() diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/filesystem/fs.mls b/hkmc2/shared/src/test/mlscript-packages/web-ide/filesystem/fs.mls new file mode 100644 index 0000000000..3c2b884395 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/filesystem/fs.mls @@ -0,0 +1,378 @@ +import "../common/JS.mls" + +open JS { Absent } + +module fs with... + +abstract class FsNode: FileNode | FolderNode + +data class FileNode( + mut val name + mut val content + mut val readonly + mut val atime + mut val mtime + mut val ctime + mut val birthtime + mut val attrs +) extends FsNode with + set this.'type = "file" + +data class FolderNode( + mut val name + mut val children + mut val readonly + mut val atime + mut val mtime + mut val ctime + mut val birthtime + mut val attrs +) extends FsNode with + set this.'type = "folder" + +val fileTree = mut [] + +let listeners = new Set() + +fun objectOrEmpty(value) = if + value is Absent then mut {} + else value + +fun textOrEmpty(value) = if + value is Absent then "" + else value + +fun splitPath(path) = + path.replace(new RegExp("^/"), "").split("/").filter of (part, ...) => part !== "" + +fun timestamps() = + let now = Date.now() + atime: now + mtime: now + ctime: now + birthtime: now + +fun updateAccessTime(node) = + set node.atime = Date.now() + +fun updateModifyTime(node) = + let now = Date.now() + set + node.mtime = now + node.ctime = now + +fun updateChangeTime(node) = + set node.ctime = Date.now() + +fun makeEvent(kind, path, node) = + mut { 'type: kind, :path, :node } + +fun notifyChange(event) = + listeners.forEach of (listener, ...) => listener(event) + +fun notifyAttr(path, node, key, value) = + let event = makeEvent("attr", path, node) + set + event.key = key + event.value = value + notifyChange(event) + +fun makeParentResult(parent, index) = + mut { :parent, :index } + +fun sortNodes(nodes) = + nodes.sort((a, b) => if + a is FolderNode and b is FileNode then -1 + a is FileNode and b is FolderNode then 1 + else a.name.localeCompare(b.name)) + +fun findChild(nodes, name) = + let + result = null + i = 0 + while i < nodes.length do + let node = nodes.at(i) + if result is null and node.name === name do + set result = node + set i += 1 + result + +fun indexOfChild(nodes, name) = + let + result = -1 + i = 0 + while i < nodes.length do + let node = nodes.at(i) + if result < 0 and node.name === name do + set result = i + set i += 1 + result + +fun findFrom(nodes, parts, index) = if + index >= parts.length then nodes + let node = findChild(nodes, parts.at(index)) + node is FileNode and index === parts.length - 1 then node + node is FolderNode(_, children, _, _, _, _, _, _) and index === parts.length - 1 then node + node is FolderNode(_, children, _, _, _, _, _, _) then findFrom(children, parts, index + 1) + else null + +fun findNode(path) = + findFrom(fileTree, splitPath(path), 0) + +fun parentChildren(parentPath) = if + parentPath is "" then fileTree + findNode("/" + parentPath) is FolderNode(_, children, _, _, _, _, _, _) then children + else null + +fun findParent(path) = + let parts = splitPath(path) + if + parts.length is 0 then null + let + parentPath = parts.slice(0, -1).join("/") + childName = parts.at(parts.length - 1) + parent = parentChildren(parentPath) + parent is null then null + let index = indexOfChild(parent, childName) + index >= 0 then makeParentResult(parent, index) + else null + +fun pathExists(path) = if findNode(path) is + null then false + else true + +fun read(path) = if findNode(path) is + FileNode(_, content, _, _, _, _, _, _) as node then + updateAccessTime(node) + textOrEmpty(content) + else throw Error("File not found: " + path) + +fun shouldMarkUncompiled(node) = if + not node.name.endsWith(".mls") then false + node.attrs.("std") is true then false + node.attrs.("compiled") is false then false + else true + +fun markUncompiled(path, node) = + if shouldMarkUncompiled(node) do + set node.attrs.("compiled") = false + notifyAttr(path, node, "compiled", false) + +fun write(path, content) = + if findNode(path) is + null then createFile(path, content, force: true) + FileNode(_, _, true, _, _, _, _, _) then throw Error("File is readonly: " + path) + FileNode as node then + set node.content = content + updateModifyTime(node) + markUncompiled(path, node) + notifyChange(makeEvent("write", path, node)) + true + else false + +fun ensureFolders(parentPath) = + let + segments = parentPath.split("/") + currentPath = "" + i = 0 + while i < segments.length do + let segment = segments.at(i) + set currentPath += "/" + segment + if not pathExists(currentPath) do + createFolder(currentPath, mut {}) + set i += 1 + +fun parentForNewFile(parentPath, options) = if + parentPath is "" then fileTree + options.("force") is true then + ensureFolders(parentPath) + parentChildren(parentPath) + else parentChildren(parentPath) + +fun setDefaultCompiled(fileName, attrs) = + if fileName.endsWith(".mls") and attrs.("compiled") is undefined do + set attrs.("compiled") = attrs.("std") is true + +fun createFile(path, contentArg, optionsArg) = if + pathExists(path) then false + let + content = textOrEmpty(contentArg) + options = objectOrEmpty(optionsArg) + parts = splitPath(path) + parts.length is 0 then false + let + fileName = parts.at(parts.length - 1) + parentPath = parts.slice(0, -1).join("/") + parent = parentForNewFile(parentPath, options) + parent is null then false + else + let + stamp = timestamps() + attrs = Object.assign(mut {}, objectOrEmpty(options.("attrs"))) + setDefaultCompiled(fileName, attrs) + let node = new mut FileNode( + fileName + content + options.("readonly") is true + stamp.atime + stamp.mtime + stamp.ctime + stamp.birthtime + attrs + ) + parent.push(node) + sortNodes(parent) + notifyChange(makeEvent("create", path, node)) + true + +fun createFolder(path, optionsArg) = if + pathExists(path) then false + let + options = objectOrEmpty(optionsArg) + parts = splitPath(path) + parts.length is 0 then false + let + folderName = parts.at(parts.length - 1) + parentPath = parts.slice(0, -1).join("/") + parent = parentChildren(parentPath) + parent is null then false + else + let + stamp = timestamps() + node = new mut FolderNode( + folderName + mut [] + options.("readonly") is true + stamp.atime + stamp.mtime + stamp.ctime + stamp.birthtime + objectOrEmpty(options.("attrs")) + ) + parent.push(node) + sortNodes(parent) + notifyChange(makeEvent("create", path, node)) + true + +fun remove(path) = if + let result = findParent(path) + result is null then false + else + let + parent = result.parent + index = result.index + node = parent.at(index) + parent.splice(index, 1) + notifyChange(makeEvent("delete", path, node)) + true + +fun rename(path, newName) = if + let node = findNode(path) + node is null then false + let + parts = splitPath(path) + newPath = "/" + parts.slice(0, -1).concat([newName]).join("/") + pathExists(newPath) then false + else + let oldName = node.name + set node.name = newName + updateChangeTime(node) + let event = makeEvent("rename", path, node) + set + event.newPath = newPath + event.oldName = oldName + notifyChange(event) + true + +fun list(path) = if findNode(path) is + FolderNode(_, children, _, _, _, _, _, _) then children + else null + +fun stat(path) = + findNode(path) + +fun acceptsFile(predicate, path, node) = if + predicate is Absent then true + typeof(predicate) is "function" then predicate(path, node) + else false + +fun eventTouches(event, path) = if + event.path === path then true + event.newPath === path then true + else false + +fun collectFiles(nodes, currentPath, predicate, result) = + nodes.forEach of (node, ...) => + let nodePath = currentPath + "/" + node.name + if node is + FileNode(_, content, _, _, _, _, _, _) and acceptsFile(predicate, nodePath, node) do + set result.(nodePath) = textOrEmpty(content) + FolderNode(_, children, _, _, _, _, _, _) do + collectFiles(children, nodePath, predicate, result) + +fun getAllFiles(predicate) = + let result = mut {} + collectFiles(fileTree, "", predicate, result) + result + +fun setAttr(path, key, value) = + if findNode(path) is + (FileNode | FolderNode) as node then + set node.attrs.(key) = value + updateChangeTime(node) + notifyAttr(path, node, key, value) + true + else false + +fun getAttr(path, key) = + if findNode(path) is + (FileNode | FolderNode) as node then node.attrs.(key) + else undefined + +fun removeAttr(path, key) = + let node = findNode(path) + if node is + (FileNode | FolderNode) as node and Reflect.has(node.attrs, key) then + Reflect.deleteProperty(node.attrs, key) + updateChangeTime(node) + notifyAttr(path, node, key, undefined) + true + (FileNode | FolderNode) as node then + Reflect.deleteProperty(node.attrs, key) + false + else false + +fun setReadonly(path, readonlyFlag) = + if findNode(path) is + (FileNode | FolderNode) as node then + set node.readonly = readonlyFlag + updateChangeTime(node) + let event = makeEvent("readonly", path, node) + set event.readonly = readonlyFlag + notifyChange(event) + true + else false + +fun resetAll() = + // Atomically empty the tree and notify subscribers with a single "reset" + // event so they can drop derived state without per-file delete fanout. + // `persistent.persistChange` ignores unknown event types, so this does not + // touch localStorage β€” the outgoing project's files remain persisted. + fileTree.splice(0, fileTree.length) + let event = mut { 'type: "reset", path: "", node: null } + notifyChange(event) + +fun subscribe(pathOrCallback, callback) = if + typeof(pathOrCallback) is "function" then + let listener = pathOrCallback + listeners.add(listener) + () => listeners.delete(listener) + typeof(pathOrCallback) is "string" and typeof(callback) is "function" then + let + path = pathOrCallback + listener = event => + if eventTouches(event, path) do + callback(event) + listeners.add(listener) + () => listeners.delete(listener) + else throw Error("Invalid arguments: expected subscribe(callback) or subscribe(path, callback)") diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/filesystem/persistent.mls b/hkmc2/shared/src/test/mlscript-packages/web-ide/filesystem/persistent.mls new file mode 100644 index 0000000000..af180606b5 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/filesystem/persistent.mls @@ -0,0 +1,153 @@ +import "./fs.mls" +import "./projects.mls" +import "../common/JS.mls" +import "../common/Logger.mls" + +open JS { Absent } + +module persistent with... + +fun isFile(node) = if node is + Absent then false + value and value.'type is "file" then true + else false + +fun isStandardLibraryNode(node) = if node is + Absent then false + value and value.attrs.("std") is true then true + else false + +fun persistedKey(path) = + projects.fileKey(projects.currentId(), path) + +fun pathsKey() = + projects.pathsKey(projects.currentId()) + +fun getPersistedPaths() = + let storedText = localStorage.getItem(pathsKey()) + if storedText is + Absent then new Set() + text then new Set(JSON.parse(text)) + +fun savePersistedPaths(paths) = + localStorage.setItem(pathsKey(), JSON.stringify(Array.from(paths))) + +fun loadPersistedFile(path) = + let storedText = localStorage.getItem(persistedKey(path)) + if storedText is + Absent then null + text then JSON.parse(text) + +fun fileData(node) = + content: node.content + readonly: node.readonly + atime: node.atime + mtime: node.mtime + ctime: node.ctime + birthtime: node.birthtime + attrs: node.attrs + +fun saveFile(path, node) = + localStorage.setItem(persistedKey(path), JSON.stringify(fileData(node))) + +fun rememberFile(paths, path, node) = + if isFile(node) do + saveFile(path, node) + paths.add(path) + savePersistedPaths(paths) + +fun forgetFile(paths, path, node) = + if isFile(node) do + localStorage.removeItem(persistedKey(path)) + paths.delete(path) + savePersistedPaths(paths) + +fun moveFile(paths, path, newPath, node) = + if isFile(node) and newPath is ~Absent as nextPath do + localStorage.removeItem(persistedKey(path)) + saveFile(nextPath, node) + paths.delete(path) + paths.add(nextPath) + savePersistedPaths(paths) + +fun updateFile(path, node) = + if isFile(node) do + saveFile(path, node) + +fun restoreTimestamps(node, fileData) = + set + node.atime = fileData.atime + node.mtime = fileData.mtime + node.ctime = fileData.ctime + node.birthtime = fileData.birthtime + +fun restoreFile(path, fileData) = + Logger.debug("Persistence", "Restoring file", path) + fs.createFile(path, fileData.content, + force: true + readonly: fileData.readonly + attrs: fileData.attrs + ) + if fs.findNode(path) is + ~null as node do restoreTimestamps(node, fileData) + true + +fun loadPersistedFiles() = + let + counter = 0 + paths = getPersistedPaths() + Logger.info("Persistence", "Loading persisted files from localStorage") + paths.forEach of (path, ...) => + if loadPersistedFile(path) is + Object as fileData do + restoreFile(path, fileData) + set counter += 1 + Logger.info("Persistence", "Loaded persisted files", counter) + counter + +fun persistChange(event) = + if not isStandardLibraryNode(event.node) do + let paths = getPersistedPaths() + if event.'type is + "create" | "write" then rememberFile(paths, event.path, event.node) + "delete" then forgetFile(paths, event.path, event.node) + "rename" then moveFile(paths, event.path, event.newPath, event.node) + "attr" | "readonly" then updateFile(event.path, event.node) + else () + +// ─── Per-project snapshot / restore (used by export & import) ─── + +fun readPathsArray(id) = + let raw = localStorage.getItem(projects.pathsKey(id)) + if raw is + Absent then [] + text then + let parsed = JSON.parse(text) + if Array.isArray(parsed) then parsed else [] + +fun savePathsArray(id, paths) = + localStorage.setItem(projects.pathsKey(id), JSON.stringify(paths)) + +fun snapshotEntry(id, path) = + let raw = localStorage.getItem(projects.fileKey(id, path)) + if raw is + Absent then null + text then + let parsed = JSON.parse(text) + mut { :path, payload: parsed } + +fun snapshotProject(id) = + let result = mut [] + readPathsArray(id).forEach of (path, ...) => + let entry = snapshotEntry(id, path) + if entry is ~null do result.push(entry) + result + +fun writeProjectFile(id, path, payload) = + localStorage.setItem(projects.fileKey(id, path), JSON.stringify(payload)) + let paths = readPathsArray(id) + if not paths.includes(path) do + paths.push(path) + savePathsArray(id, paths) + +fs.subscribe(persistChange, undefined) diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/filesystem/projects.mls b/hkmc2/shared/src/test/mlscript-packages/web-ide/filesystem/projects.mls new file mode 100644 index 0000000000..102ceb41bc --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/filesystem/projects.mls @@ -0,0 +1,291 @@ +import "../common/JS.mls" +import "../common/Logger.mls" +import "../common/SettingsStore.mls" + +open JS { Absent } + +module projects with... + +val PROJECTS_KEY = "mlscript.projects.v1" +val CURRENT_KEY = "mlscript.project.current.v1" +val SCHEMA_KEY = "mlscript.projects.schema" +val SCHEMA_VERSION = "v1" +val LEGACY_PATHS_KEY = "mlscript-fs-paths" +val LEGACY_FILE_PREFIX = "mlscript-fs:" +val FILE_PREFIX = "mlscript-fs:proj:" +val PATHS_PREFIX = "mlscript-fs-paths:proj:" +// Storage-bucket id reused by the one-shot legacy migration so existing +// users keep their files. After migration the resulting meta is a normal +// project entry β€” nothing special about this id at runtime. +val LEGACY_BUCKET_ID = "default" +val INITIAL_PROJECT_NAME = "My project" + +val state = mut + registry: mut [] + currentId: "" + +fun fileKey(id, path) = + FILE_PREFIX + id + ":" + path + +fun pathsKey(id) = + PATHS_PREFIX + id + +fun currentId() = + state.currentId + +fun listProjects() = + state.registry + +// Note: avoid Array.prototype.find β€” `runtime.safeCall(undefined)` rewrites a +// miss to `runtime.Unit`, which slips past the `Absent` pattern at call sites. +// A manual loop keeps the miss as a real `null`. +fun findProjectLoop(id) = + let + result = null + i = 0 + while i < state.registry.length do + let p = state.registry.at(i) + if result is null and p.id === id do set result = p + set i += 1 + result + +fun currentProject() = + findProjectLoop(state.currentId) + +fun today() = + new Date().toISOString().slice(0, 10) + +fun makeProject(id, name) = + mut + id: id + name: name + description: "" + color: "accent" + icon: "" + createdAt: today() + modifiedAt: today() + activeFile: "/main.mls" + +fun writeText(key, value) = + localStorage.setItem(key, value) + +fun readText(key) = + localStorage.getItem(key) + +fun removeText(key) = + localStorage.removeItem(key) + +fun writeJSON(key, value) = + writeText(key, JSON.stringify(value)) + +fun readJSON(key) = if readText(key) is + Absent then null + text then JSON.parse(text) + +fun saveRegistry() = + writeJSON(PROJECTS_KEY, state.registry) + writeText(CURRENT_KEY, state.currentId) + +fun loadRegistry() = + let parsed = readJSON(PROJECTS_KEY) + if parsed is + Absent then false + else + if Array.isArray(parsed) then + set state.registry = parsed + true + else false + +fun bumpModified(id) = + state.registry.forEach of (p, ...) => + if p.id === id do set p.modifiedAt = today() + +fun setActiveFile(id, path) = + state.registry.forEach of (p, ...) => + if p.id === id do set p.activeFile = path + +fun migrateLegacyFile(path) = + let key = LEGACY_FILE_PREFIX + path + if readText(key) is + ~Absent as blob do + writeText(fileKey(LEGACY_BUCKET_ID, path), blob) + removeText(key) + +fun migrateLegacyIfNeeded() = + if readText(SCHEMA_KEY) is + ~Absent then false + else + let legacyText = readText(LEGACY_PATHS_KEY) + if legacyText is + Absent then false + text then + Logger.info("Projects", "Migrating legacy persistence into v1 namespace", LEGACY_BUCKET_ID) + let + parsed = JSON.parse(text) + paths = if Array.isArray(parsed) then parsed else [] + paths.forEach of (path, ...) => migrateLegacyFile(path) + writeText(pathsKey(LEGACY_BUCKET_ID), JSON.stringify(paths)) + removeText(LEGACY_PATHS_KEY) + Logger.info("Projects", "Migrated legacy files", paths.length) + true + +// Make sure the registry has at least one project. The only time this fires +// after the first boot is when a user has wiped localStorage, since the +// delete guard refuses to remove the last remaining project. If we just +// migrated legacy data we adopt the migration bucket; otherwise we mint a +// fresh project with a random id β€” no project id is ever "the" default. +fun ensureAtLeastOneProject(migrated) = + if state.registry.length === 0 do + if migrated then + state.registry.push(makeProject(LEGACY_BUCKET_ID, INITIAL_PROJECT_NAME)) + else + let id = generateUniqueId() + state.registry.push(makeProject(id, INITIAL_PROJECT_NAME)) + +fun setCurrentTo(id) = + set state.currentId = id + +fun fallbackToFirstProject() = + if state.registry.length > 0 do + setCurrentTo(state.registry.at(0).id) + +fun shouldReopenLastProject() = + SettingsStore.getSetting("workbench.reopenLastProject", "true") !== "false" + +fun selectInitialCurrent() = + if not shouldReopenLastProject() then fallbackToFirstProject() + else + let stored = readText(CURRENT_KEY) + if stored is + Absent then fallbackToFirstProject() + id then + let known = state.registry.some of (p, ...) => p.id === id + if known then setCurrentTo(id) + else fallbackToFirstProject() + +fun markSchema() = + writeText(SCHEMA_KEY, SCHEMA_VERSION) + +fun bootstrap() = + Logger.info("Projects", "Bootstrapping project registry") + let migrated = migrateLegacyIfNeeded() + loadRegistry() + ensureAtLeastOneProject(migrated) + selectInitialCurrent() + markSchema() + saveRegistry() + Logger.info("Projects", "Project registry ready", + "count:", state.registry.length, + "current:", state.currentId) + +// ─── Registry mutations ─────────────────────────────────────── + +fun randomId() = + "proj-" + Math.random().toString(36).slice(2, 8) + +fun idExists(id) = + state.registry.some of (p, ...) => p.id === id + +fun generateUniqueId() = + let + candidate = randomId() + collisions = 0 + while idExists(candidate) do + set candidate = randomId() + set collisions += 1 + if collisions > 16 do throw Error("Could not allocate a unique project id") + candidate + +fun findProject(id) = + findProjectLoop(id) + +fun isCurrent(id) = + id === state.currentId + +fun createProject(name) = + let + id = generateUniqueId() + project = makeProject(id, name) + state.registry.push(project) + saveRegistry() + Logger.info("Projects", "Created project", id, name) + project + +// For Import. The incoming meta keeps its descriptive fields but is given a +// fresh id (to avoid collisions with any existing project) and a current +// modifiedAt. Missing fields fall back to the same defaults as makeProject. +fun adoptProject(incoming) = + let + id = generateUniqueId() + fallbackName = if incoming.name is ~Absent then incoming.name else "Imported project" + project = makeProject(id, fallbackName) + if incoming.description is ~Absent do set project.description = incoming.description + if incoming.color is ~Absent do set project.color = incoming.color + if incoming.icon is ~Absent do set project.icon = incoming.icon + if incoming.createdAt is ~Absent do set project.createdAt = incoming.createdAt + if incoming.activeFile is ~Absent do set project.activeFile = incoming.activeFile + state.registry.push(project) + saveRegistry() + Logger.info("Projects", "Adopted imported project", id, project.name) + project + +fun deleteProject(id) = if + isCurrent(id) then + Logger.warn("Projects", "Refused to delete current project", id) + false + state.registry.length <= 1 then + Logger.warn("Projects", "Refused to delete the only remaining project", id) + false + else + let index = state.registry.findIndex of (p, ...) => p.id === id + if index < 0 then + Logger.warn("Projects", "Unknown project id for delete", id) + false + else + state.registry.splice(index, 1) + purgeProjectStorage(id) + saveRegistry() + Logger.info("Projects", "Deleted project", id) + true + +fun commitCurrent(id) = + set state.currentId = id + saveRegistry() + +fun renameProject(id, name) = + let trimmed = if name is Absent then "" else name.trim() + if trimmed is + "" then + Logger.warn("Projects", "Refused to rename project to empty name", id) + false + else + let project = findProjectLoop(id) + if project is + null then + Logger.warn("Projects", "Unknown project id for rename", id) + false + else + set + project.name = trimmed + project.modifiedAt = today() + saveRegistry() + Logger.info("Projects", "Renamed project", id, trimmed) + true + +fun purgeProjectStorage(id) = + let pathsText = readText(pathsKey(id)) + if pathsText is + ~Absent as text do + let parsed = JSON.parse(text) + if Array.isArray(parsed) do + parsed.forEach of (path, ...) => removeText(fileKey(id, path)) + removeText(pathsKey(id)) + +fun fileCount(id) = + let pathsText = readText(pathsKey(id)) + if pathsText is + Absent then 0 + text then + let parsed = JSON.parse(text) + if Array.isArray(parsed) then parsed.length + else 0 diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/index.html b/hkmc2/shared/src/test/mlscript-packages/web-ide/index.html new file mode 100644 index 0000000000..f5bded4aeb --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/index.html @@ -0,0 +1,94 @@ + + + + + + + MLscript Web IDE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/main.mls b/hkmc2/shared/src/test/mlscript-packages/web-ide/main.mls new file mode 100644 index 0000000000..e7bb353bbf --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/main.mls @@ -0,0 +1,512 @@ +import "./filesystem/fs.mls" +import "./filesystem/projects.mls" +import "./filesystem/persistent.mls" +import "./execution/runner.mls" +import "./compiler/index.mls" +import "./common/JS.mls" +import "./common/Logger.mls" +import "./common/SettingsStore.mls" + +open JS { Absent } + +module main with... + +fun dynamicImport(specifier) = + let importModule = Function("specifier", "return import(specifier)") + importModule(specifier) + +fun compilerModule(loaded) = if loaded.'default is + Absent then loaded + defaultModule then defaultModule + +fun loadMLscript() = + dynamicImport("./build/MLscript.mjs").then(loaded => compilerModule(loaded)) + +fun loadAnalysis() = + dynamicImport("./analysis/index.mjs?v=symbol-index").then(loaded => compilerModule(loaded)) + +fun folderAttrs() = + mut { collapsed: true } + +fun standardAttrs() = + mut { std: true, compiled: true } + +fun createStandardFile(path, content) = + Logger.debug("Main", "Loading standard library file", path) + fs.createFile(path, content, + force: true + attrs: standardAttrs() + ) + +fun loadStandardLibraryBody(compiler) = + fs.createFolder("/std", force: true, attrs: folderAttrs()) + fs.createFile("/std/Prelude.mls", compiler.std.prelude, + force: true + attrs: standardAttrs() + ) + compiler.std.files.forEach of (entry, ...) => + createStandardFile(entry.at(0), entry.at(1)) + +fun loadStandardLibrary(compiler) = + Logger.info("Main", "Loading standard library files") + js.try_catch( + () => + loadStandardLibraryBody(compiler) + Logger.info("Main", "Loaded standard library files") + error => + Logger.error("Main", "Failed to load standard library files", error) + throw error + ) + +let defaultMainSource = "import \"./std/Predef.mls\"\n\n" + + "open Predef\n\n" + + "print of \"Welcome to MLscript Web IDE!\"\n" + + "print of \"============================\"\n\n" + + "print of \"Press Ctrl-S to compile.\"\n" + + "print of \"Press Ctrl-E to execute.\"\n" + +fun ensureDefaultMainFile() = + if persistent.loadPersistedFiles() is 0 do + fs.createFile("/main.mls", defaultMainSource, force: true) + +fun attrsOf(node) = if + node is Absent then null + node.attrs is + Absent then null + attrs then attrs + +fun isStandardNode(node) = if attrsOf(node) is + null then false + attrs and attrs.("std") is true then true + else false + +fun isCompiledNode(node) = if attrsOf(node) is + null then false + attrs and attrs.("compiled") is true then true + else false + +fun shouldCompile(path) = + let node = fs.stat(path) + if + not path.endsWith(".mls") then false + isStandardNode(node) then false + isCompiledNode(node) then false + else true + +fun isCompilationFile(path) = if + path.endsWith(".mls") then true + path.endsWith(".mjs") then true + else false + +fun filesForCompilation() = + fs.getAllFiles((path, ...) => isCompilationFile(path)) + +fun parentPath(path) = + let index = path.lastIndexOf("/") + if index <= 0 then "/" + else path.slice(0, index) + +fun normalizePath(path) = + let + parts = path.split("/") + stack = mut [] + parts.forEach of (part, ...) => + if + part is "" then () + part is "." then () + part is ".." then stack.pop() + else stack.push(part) + "/" + stack.join("/") + +fun resolveImportPath(fromPath, specifier) = if + specifier.startsWith(".") then normalizePath(parentPath(fromPath) + "/" + specifier) + specifier.startsWith("/") then normalizePath(specifier) + else normalizePath("/" + specifier) + +fun mlsImportTarget(files, fromPath, specifier) = + let resolved = resolveImportPath(fromPath, specifier) + if + resolved.endsWith(".mls") and Reflect.has(files, resolved) then resolved + not resolved.endsWith(".mjs") and Reflect.has(files, resolved + ".mls") then resolved + ".mls" + else null + +fun importSpecifiers(content) = + let + specs = mut [] + importPattern = new RegExp("^\\s*import\\s+[\"']([^\"']+)[\"']") + content.split("\n").forEach of (line, ...) => + if not line.trimStart().startsWith("//") do + if line.match(importPattern) is + ~null as match do specs.push(match.at(1)) + specs + +fun collectImportClosure(files, targetPath) = + let + targets = new Set() + queue = mut [] + if targetPath is ~Absent as path and path !== "" and path.endsWith(".mls") and Reflect.has(files, path) do + targets.add(path) + queue.push(path) + while queue.length > 0 do + let current = queue.shift() + importSpecifiers(files.(current)).forEach of (specifier, ...) => + if mlsImportTarget(files, current, specifier) is + ~null as dependency do + if not targets.has(dependency) do + targets.add(dependency) + queue.push(dependency) + Array.from(targets) + +fun collectFilesForCompilation(targetPath) = + let + files = filesForCompilation() + targets = collectImportClosure(files, targetPath) + [files, targets] + +fun markPathCompiled(path) = + let node = fs.stat(path) + if not isStandardNode(node) do fs.setAttr(path, "compiled", true) + +fun markAsCompiled(paths) = + paths.forEach of (path, ...) => markPathCompiled(path) + +fun markPathUncompiled(path) = + let node = fs.stat(path) + if not isStandardNode(node) do fs.setAttr(path, "compiled", false) + +fun markAsUncompiled(paths) = + paths.forEach of (path, ...) => markPathUncompiled(path) + +fun diagnosticsInspector() = + document.querySelector("diagnostics-inspector") + +fun setDiagnostics(diagnosticsPerFile) = + if diagnosticsInspector() is + ~null as panel do panel.setDiagnostics(diagnosticsPerFile) + +fun revealDiagnostics(diagnosticsPerFile) = + document.dispatchEvent(new CustomEvent("sidebar-toggle-requested", mut + detail: mut { side: "right", panel: "problems", forceOpen: true } + )) + if diagnosticsInspector() is ~null as panel do + panel.setMode("workspace") + panel.setDiagnostics(diagnosticsPerFile) + +fun bottomPanel() = + document.querySelector("bottom-panel") + +fun showLoggingPanel() = + if bottomPanel() is + ~null as panel do panel.showLogging() + +fun compilationStatusDetail(status, error) = + mut { :status, :error } + +fun dispatchCompilationStatus(status, error) = + document.dispatchEvent(new CustomEvent("compilation-status-change", mut + detail: compilationStatusDetail(status, error) + )) + +fun errorName(error) = if error.name is + Absent then "CompilerError" + name then name + +fun errorMessage(error) = if error.message is + Absent then String(error) + message then message + +fun errorStack(error) = if error.stack is + Absent then "" + stack then stack + +fun fatalDiagnostic(path, error) = + mut + kind: "internal" + source: "compiler" + mainMessage: "Compiler internal error: " + errorMessage(error) + excerpt: errorStack(error) + line: 1 + allMessages: [ + mut + messageBits: [ + mut { text: errorName(error) + ": " + errorMessage(error) } + mut { text: errorStack(error) } + ] + location: mut { start: 0, end: 0 } + ] + +fun compilerFatalDiagnostics(path, error) = + [ + mut + path: if path is "" then "/compiler" else path + diagnostics: [fatalDiagnostic(path, error)] + ] + +fun filePathOfDiagnostics(fileData) = if fileData.path is + Absent then "Unknown file" + path then path + +fun diagnosticKind(diagnostic) = + let kind = if diagnostic.kind is + Absent then "info" + value then value + if kind is + "error" then "error" + "internal" then "internal" + "warning" then "warning" + else "info" + +fun isBlockingDiagnostic(diagnostic) = if diagnosticKind(diagnostic) is + "error" then true + "internal" then true + else false + +fun hasBlockingDiagnostics(diagnosticsPerFile) = + let found = false + if diagnosticsPerFile is ~Absent do + diagnosticsPerFile.forEach of (fileData, ...) => + if not found and fileData.diagnostics is ~Absent as diagnostics do + diagnostics.forEach of (diagnostic, ...) => + if not found and isBlockingDiagnostic(diagnostic) do set found = true + found + +fun messageText(message) = + let text = "" + if message.messageBits is ~Absent as bits do + bits.forEach of (bit, ...) => + if bit.text is ~Absent as bitText do set text += bitText + if bit.code is ~Absent as bitCode do set text += bitCode + text + +fun firstDiagnosticText(diagnostic) = if + diagnostic.allMessages is Absent then "Diagnostic" + diagnostic.allMessages.length === 0 then "Diagnostic" + else messageText(diagnostic.allMessages.at(0)) + +fun primaryLocation(diagnostic) = + let result = null + if diagnostic.allMessages is ~Absent as messages do + messages.forEach of (message, ...) => + if result is null do + if message.location is ~Absent as location do set result = location + result + +fun sourceText(filePath) = + if typeof(filePath) is "string" and fs.pathExists(filePath) then fs.read(filePath) + else "" + +fun lineColumn(line, column) = + mut { :line, :column } + +fun lineColumnFromOffset(text, offset) = + let lines = text.substring(0, offset).split("\n") + lineColumn(lines.length, lines.at(lines.length - 1).length + 1) + +fun diagnosticLocation(diagnostic, filePath) = if diagnostic.line is + ~Absent as line then lineColumn(line, 1) + else if primaryLocation(diagnostic) is + ~null as location then lineColumnFromOffset(sourceText(filePath), location.start) + else null + +fun diagnosticMainMessage(diagnostic) = if diagnostic.mainMessage is + Absent then firstDiagnosticText(diagnostic) + message then message + +fun diagnosticDetailText(diagnostic) = + let text = "" + if diagnostic.allMessages is ~Absent as messages do + messages.forEach of (message, ...) => + let content = messageText(message) + if content !== "" do + if text === "" then set text = content + else set text += " " + content + if text === "" then diagnosticMainMessage(diagnostic) else text + +fun diagnosticLocationSuffix(filePath, diagnostic) = if diagnosticLocation(diagnostic, filePath) is + ~null as location then ":" + location.line + ":" + location.column + else "" + +fun diagnosticOutputLine(filePath, diagnostic) = + "[" + diagnosticKind(diagnostic) + "] " + filePath + diagnosticLocationSuffix(filePath, diagnostic) + " - " + diagnosticMainMessage(diagnostic) + +fun emitDiagnosticOutput(diagnosticsPerFile) = + if bottomPanel() is ~null as panel do + panel.showOutput() + panel.log("error", "Compilation failed with diagnostics:") + diagnosticsPerFile.forEach of (fileData, ...) => + let filePath = filePathOfDiagnostics(fileData) + if fileData.diagnostics is ~Absent as diagnostics do + diagnostics.forEach of (diagnostic, ...) => + if isBlockingDiagnostic(diagnostic) do + panel.log("error", diagnosticOutputLine(filePath, diagnostic)) + let detail = diagnosticDetailText(diagnostic) + if detail !== diagnosticMainMessage(diagnostic) do panel.log("error", detail) + +fun handleCompileFatal(targetPath, error) = + markPathUncompiled(targetPath) + revealDiagnostics(compilerFatalDiagnostics(targetPath, error)) + Logger.error("Compiler", "Compiler internal error", + mut + name: errorName(error) + message: errorMessage(error) + stack: errorStack(error) + ) + dispatchCompilationStatus("fatal", error) + showLoggingPanel() + +fun compileSuccess(targetPaths, response) = + let diagnostics = response.result + setDiagnostics(diagnostics) + if hasBlockingDiagnostics(diagnostics) then + markAsUncompiled(targetPaths) + revealDiagnostics(diagnostics) + emitDiagnosticOutput(diagnostics) + dispatchCompilationStatus("error", null) + false + else + markAsCompiled(targetPaths) + dispatchCompilationStatus("done", null) + true + +fun autoRunAfterCompileEnabled() = + SettingsStore.getSetting("compile.autoRunAfterCompile", "false") === "true" + +fun executeFile(filePath) = + document.dispatchEvent(new CustomEvent("execute-requested", mut + bubbles: true + detail: mut { :filePath } + )) + +fun compileTarget(targetPath) = + let + collected = collectFilesForCompilation(targetPath) + allFiles = collected.at(0) + targetPaths = collected.at(1) + index.compile(targetPaths, allFiles) + .then(response => + if compileSuccess(targetPaths, response) and autoRunAfterCompileEnabled() do executeFile(targetPath) + ) + .'catch(error => handleCompileFatal(targetPath, error)) + +fun isMlsPath(path) = + typeof(path) is "string" and path.endsWith(".mls") + +fun handleCompileRequested(event) = + let filePath = event.detail.filePath + if + isMlsPath(filePath) then compileTarget(filePath) + else Logger.warn("Compiler", "Compile skipped: active file is not an .mls file", filePath) + + +fun compiledPath(filePath) = if + filePath.endsWith(".mls") then filePath.slice(0, filePath.length - 4) + ".mjs" + else filePath + +fun compiledOutputHasCompileError(mjsPath) = + if fs.pathExists(mjsPath) then sourceText(mjsPath).includes("compilation yielded an error") + else false + +fun shouldCompileBeforeExecution(filePath, mjsPath) = + if isMlsPath(filePath) then + if shouldCompile(filePath) then true + else compiledOutputHasCompileError(mjsPath) + else false + +fun runCompiledFile(sourcePath, mjsPath) = + if fs.pathExists(mjsPath) then runner.execute(mjsPath, sourcePath) + else Logger.error("Execution", "Compiled file not found after compilation", mjsPath) + +fun compileThenExecute(filePath, mjsPath) = + Logger.warn("Execution", "Compiled file not found, compiling first", mjsPath) + let + collected = collectFilesForCompilation(filePath) + allFiles = collected.at(0) + targetPaths = collected.at(1) + compilePromise = index.compile(targetPaths, allFiles).then of response => + if compileSuccess(targetPaths, response) do runCompiledFile(filePath, mjsPath) + compilePromise.'catch(error => handleCompileFatal(filePath, error)) + +fun expandConsolePanel() = + if document.querySelector("bottom-panel") is + ~null as panel do panel.showOutput() + +fun handleExecuteRequested(event) = + expandConsolePanel() + let filePath = event.detail.filePath + if typeof(filePath) is ~"string" then Logger.error("Execution", "Invalid file path for execution", filePath) + else + let mjsPath = compiledPath(filePath) + if shouldCompileBeforeExecution(filePath, mjsPath) then compileThenExecute(filePath, mjsPath) + else if fs.pathExists(mjsPath) then runner.execute(mjsPath, filePath) + else compileThenExecute(filePath, mjsPath) + +fun handleOpenFileAtLocation(event) = + let editorPanel = document.querySelector("editor-panel") + if editorPanel is + ~null as panel do panel.openFileAtLine(event.detail.filePath, event.detail.line, event.detail.column, event.detail.length) + +fun dispatchProjectChange() = + let detail = mut + id: projects.currentId() + project: projects.currentProject() + document.dispatchEvent(new CustomEvent("current-project-changed", mut { :detail })) + +let runtimeRef = mut { compiler: null, analysis: null } + +fun switchProject(nextId) = + if projects.currentId() === nextId then () + else + let + current = projects.currentId() + target = projects.findProject(nextId) + if target is + null then Logger.warn("Projects", "Switch ignored: unknown project id", nextId) + else + Logger.info("Projects", "Switching project", current, "β†’", nextId) + // 1. Tear down the in-memory tree. Subscribers (FileExplorer, + // EditorPanel) react to the `reset` event by clearing their state. + // Persistence ignores it, so the outgoing project's files remain + // safe in localStorage. + fs.resetAll() + setDiagnostics([]) + // 2. Re-inject the standard library. Std files carry `attrs.std = true` + // so they are skipped by persistence. + if runtimeRef.compiler is ~null as compiler do loadStandardLibrary(compiler) + // 3. Commit the new current id so subsequent persistence reads/writes + // target the right namespace. + projects.commitCurrent(nextId) + // 4. Restore the new project's persisted files. If none yet, seed + // /main.mls so the editor has something to open. + let restored = persistent.loadPersistedFiles() + if restored is 0 do fs.createFile("/main.mls", defaultMainSource, force: true) + // 5. Reinitialize the analysis worker so its symbol index reflects + // the new file set instead of incrementally folding the reset. + if runtimeRef.analysis is ~null as analysis do analysis.reinitialize() + // 6. Notify the toolbar pill and anything else that cares. + dispatchProjectChange() + Logger.info("Projects", "Switched", nextId, "files:", restored) + +fun handleProjectOpenRequested(event) = + if + event.detail is Absent then Logger.warn("Projects", "project-open-requested missing detail") + event.detail.id is Absent then Logger.warn("Projects", "project-open-requested missing id") + else switchProject(event.detail.id) + +fun start(compiler, analysis) = + set + runtimeRef.compiler = compiler + runtimeRef.analysis = analysis + projects.bootstrap() + dispatchProjectChange() + loadStandardLibrary(compiler) + ensureDefaultMainFile() + analysis.start() + + document.addEventListener("compile-requested", handleCompileRequested) + document.addEventListener("execute-requested", handleExecuteRequested) + document.addEventListener("terminate-requested", () => runner.terminate()) + document.addEventListener("open-file-at-location", handleOpenFileAtLocation) + document.addEventListener("project-open-requested", handleProjectOpenRequested) + +let startPromise = loadMLscript().then(compiler => loadAnalysis().then(analysis => start(compiler, analysis))) +startPromise.'catch(error => Logger.error("Main", "Failed to load MLscript compiler or analysis module", error)) diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/manifest.json b/hkmc2/shared/src/test/mlscript-packages/web-ide/manifest.json new file mode 100644 index 0000000000..41c30d0c52 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/manifest.json @@ -0,0 +1,6 @@ +{ + "name": "web-ide", + "main": "editor/Highlight.mls", + "moduleName": "Highlight", + "vendors": [] +} diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/mockWorkbenchData.mls b/hkmc2/shared/src/test/mlscript-packages/web-ide/mockWorkbenchData.mls new file mode 100644 index 0000000000..5b858be482 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/mockWorkbenchData.mls @@ -0,0 +1,51 @@ +module mockWorkbenchData with... + +val sourceChanges = [ + mut { path: "/main.mls", status: "modified", summary: "Edited welcome program" } + mut { path: "/std/Predef.mls", status: "modified", summary: "Experiment with print helpers" } + mut { path: "/std/Option.mls", status: "modified", summary: "Refine option formatting" } + mut { path: "/examples/pipeline.mls", status: "added", summary: "Add pipeline example draft" } + mut { path: "/notes/compiler-log.md", status: "deleted", summary: "Remove obsolete notes" } + mut { path: "/std/Rendering.mls", status: "modified", summary: "Adjust renderer experiment" } + mut { path: "/std/Runtime.mls", status: "modified", summary: "Try alternate runtime hook" } + mut { path: "/std/XML.mls", status: "modified", summary: "Update XML attribute sketch" } + mut { path: "/examples/records.mls", status: "added", summary: "Draft record example" } + mut { path: "/examples/channel.mls", status: "added", summary: "Draft CSP channel example" } + mut { path: "/notes/old-outline.md", status: "deleted", summary: "Remove old outline note" } +] + +val outlineItems = [ + mut { name: "imports", kind: "section", line: 1, detail: "Imports and module setup" } + mut { name: "Predef", kind: "namespace", line: 3, detail: "Opened standard namespace" } + mut { name: "welcome banner", kind: "expression", line: 5, detail: "First console print" } + mut { name: "separator", kind: "expression", line: 6, detail: "Console divider" } + mut { name: "compile hint", kind: "expression", line: 8, detail: "Compile shortcut message" } + mut { name: "execute hint", kind: "expression", line: 9, detail: "Execute shortcut message" } + mut { name: "runtime import", kind: "import", line: 12, detail: "Mock generated import" } + mut { name: "print helper", kind: "function", line: 18, detail: "Mock helper symbol" } + mut { name: "diagnostic hook", kind: "function", line: 24, detail: "Mock compiler hook" } + mut { name: "execute hook", kind: "function", line: 31, detail: "Mock runner hook" } + mut { name: "status update", kind: "event", line: 39, detail: "Mock status event" } + mut { name: "console append", kind: "event", line: 46, detail: "Mock console event" } + mut { name: "final expression", kind: "expression", line: 52, detail: "Mock final expression" } + mut { name: "module footer", kind: "section", line: 60, detail: "Mock module footer" } +] + +val examples = [ + mut { id: "hello", title: "Hello IDE", file: "hello.mls", description: "Small executable program that prints several lines to the output panel.", preview: "print of \"Hello from MLscript\"" } + mut { id: "adt", title: "Algebraic Data", file: "option-demo.mls", description: "Defines a compact option-like shape and pattern matches on values.", preview: "case value of Some(x) then x else 0" } + mut { id: "iterator", title: "Iterator Pipeline", file: "iter-pipeline.mls", description: "Sketches a pipeline over iterable values with map and filter steps.", preview: "items |> Iter.map(fn) |> Iter.toArray" } + mut { id: "rendering", title: "Rendering Tree", file: "rendering-tree.mls", description: "Builds a nested rendering term and sends it through the renderer.", preview: "Rendering.render(document)" } + mut { id: "records", title: "Records", file: "records.mls", description: "Shows object-like records and simple field updates.", preview: "user.with(name = \"Ada\")" } + mut { id: "xml", title: "XML Builder", file: "xml-builder.mls", description: "Creates a compact XML tree and renders it as text.", preview: "XML.node(\"main\", [], children)" } + mut { id: "channels", title: "Channels", file: "channels.mls", description: "Mocks a concurrent process example using CSP-style names.", preview: "spawn producer(channel)" } + mut { id: "shapes", title: "Shapes", file: "shapes.mls", description: "Explores shape metadata and field labels.", preview: "Shape.of(record)" } +] + +val terminalLines = [ + "MLscript Web IDE mock terminal" + "$ pwd" + "/workspace" + "$ mlscript --version" + "mlscript-dev" +] diff --git a/hkmc2/shared/src/test/mlscript-packages/web-ide/style.css b/hkmc2/shared/src/test/mlscript-packages/web-ide/style.css new file mode 100644 index 0000000000..98def21869 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-packages/web-ide/style.css @@ -0,0 +1,4754 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +:root { + --z-tooltip: 800; + --monospace: 'Google Sans Code', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + --sans-serif: 'Rubik', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + --ide-bg: #f6f7f4; + --ide-surface: #fffefd; + --ide-surface-subtle: #eef1ed; + --ide-surface-muted: #e3e8e2; + --ide-rail-bg: #e9eee9; + --ide-editor-bg: #fffefd; + --ide-border: #d3dad1; + --ide-border-strong: #b9c4b8; + --ide-text: #242620; + --ide-text-muted: #62685d; + --ide-text-subtle: #808779; + --ide-accent: #2d6f73; + --ide-accent-soft: #dcecea; + --ide-accent-strong: #1f5558; + --ide-danger: #d13438; + --ide-danger-soft: #fde8e8; + --ide-warning: #b95000; + --ide-warning-soft: #fff1df; + --ide-success: #2f7d46; + --ide-success-soft: #e4f3e9; + --ide-info: #1269a8; + --ide-internal: #7a5195; + --ide-radius-xs: 3px; + --ide-radius-sm: 6px; + --ide-radius-md: 8px; + --ide-shadow-sm: 0 1px 2px rgba(24, 26, 22, 0.06); + --ide-shadow-md: 0 8px 24px rgba(24, 26, 22, 0.12); + --activity-rail-width: 44px; + --file-explorer-width: 250px; + --ide-right-panel-width: 300px; + --ide-bottom-panel-height: 200px; + --ide-bottom-panel-max-height: 400px; + --ide-bottom-panel-collapsed-height: 40px; + --ide-panel-header-height: 40px; + --ide-toolbar-height: 44px; + --ide-status-bar-height: 24px; + --editor-font-size: 14px; + --editor-font-family: var(--monospace); + --editor-font-variation-normal: 'wght' 400; + --editor-font-variation-bold: 'wght' 700; + --editor-font-variation-italic: 'wght' 400; + --editor-font-variation-bold-italic: 'wght' 700; + --editor-line-height: 1.45; + --editor-font-ligatures: normal; + --editor-letter-spacing: 0px; + --editor-ruler-color: color-mix(in srgb, var(--ide-border-strong) 50%, transparent); + --editor-rulers-background: none; + --panel-header-height: var(--ide-panel-header-height); + font-family: var(--sans-serif); +} + +button { + font-family: inherit; +} + +button > i[class^="icon-"], +button > i[class*=" icon-"] { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + line-height: 1; +} + +button > i[class^="icon-"]::before, +button > i[class*=" icon-"]::before { + display: block; + line-height: 1; +} + +body { + background: var(--ide-bg); + color: var(--ide-text); + height: 100vh; + overflow: hidden; +} + +ide-workbench { + display: block; + height: 100vh; + min-height: 0; +} + +.app-layout { + display: flex; + flex-direction: column; + height: 100vh; + overflow: hidden; +} + +.app-container { + --left-sidebar-track: var(--file-explorer-width, 250px); + --right-sidebar-track: var(--ide-right-panel-width, 300px); + display: grid; + grid-template-columns: + var(--activity-rail-width, 44px) + var(--left-sidebar-track) + minmax(0, 1fr) + var(--right-sidebar-track) + var(--activity-rail-width, 44px); + flex: 1; + min-height: 0; + overflow: hidden; + gap: 0; + background: var(--ide-bg); + position: relative; +} + +.app-container.left-sidebar-closed { + --left-sidebar-track: 0px; +} + +.app-container.right-sidebar-closed { + --right-sidebar-track: 0px; +} + +.activity-rail { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + min-width: var(--activity-rail-width, 44px); + padding: 6px 5px; + background: var(--ide-rail-bg); + border-color: var(--ide-border); + min-height: 0; +} + +.activity-rail-left { + grid-column: 1; + border-right: 1px solid var(--ide-border); +} + +.activity-rail-right { + grid-column: 5; + border-left: 1px solid var(--ide-border); +} + +.activity-button { + width: 32px; + height: 32px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid transparent; + border-radius: var(--ide-radius-sm); + background: transparent; + color: var(--ide-text-muted); + cursor: pointer; + font-size: 1rem; + line-height: 1; +} + +.activity-button:hover { + color: var(--ide-text); + background: var(--ide-surface-subtle); +} + +.activity-button.active { + color: var(--ide-accent-strong); + background: var(--ide-accent-soft); + border-color: color-mix(in srgb, var(--ide-accent) 38%, var(--ide-border)); + box-shadow: inset 3px 0 0 var(--ide-accent); +} + +.activity-rail-right .activity-button.active { + box-shadow: inset -3px 0 0 var(--ide-accent); +} + +.activity-button i { + width: 1rem; + height: 1rem; +} + +.sidebar-panel:not(.active), +.app-container.left-sidebar-closed .left-sidebar-panel, +.app-container.right-sidebar-closed .right-sidebar-panel { + display: none; +} + +.left-sidebar-panel { + grid-column: 2; + grid-row: 1; +} + +.right-sidebar-panel { + grid-column: 4; + grid-row: 1; +} + +.sidebar-resize-handle { + align-self: stretch; + position: relative; + top: auto; + z-index: 25; +} + +.left-sidebar-resize { + grid-column: 2; + grid-row: 1; + justify-self: end; + transform: translateX(50%); +} + +.right-sidebar-resize { + grid-column: 4; + grid-row: 1; + justify-self: start; + transform: translateX(-50%); +} + +.app-container.left-sidebar-closed .left-sidebar-resize, +.app-container.right-sidebar-closed .right-sidebar-resize { + display: none; +} + +@media (max-width: 760px) { + .app-container { + grid-template-columns: + var(--activity-rail-width, 44px) + minmax(0, 1fr) + var(--activity-rail-width, 44px); + } + + editor-workbench { + grid-column: 2; + } + + .left-sidebar-panel, + .right-sidebar-panel { + position: absolute; + top: 0; + bottom: 0; + width: min(280px, calc(100vw - var(--activity-rail-width, 44px))); + max-width: calc(100vw - var(--activity-rail-width, 44px)); + z-index: 30; + box-shadow: var(--ide-shadow-md); + } + + .left-sidebar-panel { + left: var(--activity-rail-width, 44px); + grid-column: auto; + } + + .right-sidebar-panel { + right: var(--activity-rail-width, 44px); + grid-column: auto; + } + + .activity-rail-right { + grid-column: 3; + } +} + +.workbench-status-bar { + min-height: var(--ide-status-bar-height); + height: var(--ide-status-bar-height); + display: flex; + align-items: center; + gap: 10px; + padding: 0 10px; + background: var(--ide-surface-subtle); + border-top: 1px solid var(--ide-border); + color: var(--ide-text-muted); + font-size: 0.75rem; + line-height: 1; + flex: 0 0 auto; +} + +.workbench-status-bar .status-segment { + display: inline-flex; + align-items: center; + gap: 4px; + min-height: 20px; + white-space: nowrap; +} + +.workbench-status-bar .status-spacer ~ .status-segment:not([hidden]) { + border-left: 1px solid color-mix(in srgb, var(--ide-border-strong) 64%, transparent); + padding-left: 10px; +} + +.workbench-status-bar .status-spacer { + flex: 1; + min-width: 0; +} + +.workbench-status-bar .active-file-status { + min-width: 0; + max-width: 40vw; + overflow: hidden; + text-overflow: ellipsis; +} + +.workbench-status-bar .status-action { + border: 1px solid transparent; + border-radius: var(--ide-radius-xs); + background: transparent; + color: var(--ide-text-muted); + cursor: pointer; + padding: 0 5px; +} + +.workbench-status-bar .status-action:hover, +.workbench-status-bar .status-action.active { + color: var(--ide-accent-strong); + background: var(--ide-accent-soft); + border-color: color-mix(in srgb, var(--ide-accent) 35%, var(--ide-border)); +} + +.workbench-status-bar .status-action i { + width: 0.8rem; + height: 0.8rem; +} + +/* File Explorer */ +file-explorer { + display: flex; + flex-direction: column; + background: var(--ide-surface); + border-right: 1px solid var(--ide-border); + position: relative; + min-width: 0; + min-height: 0; +} + +file-explorer .header { + display: flex; + align-items: center; + gap: 0.125rem; + padding: 8px 12px; + background: var(--ide-surface-subtle); + border-bottom: 1px solid var(--ide-border); + height: var(--panel-header-height); +} + +file-explorer .header h2 { + font-size: 14px; + font-weight: 600; +} + +file-explorer.collapsed .header h2 { + display: none; +} + +file-explorer .button:first-of-type { + margin-left: auto; +} + +file-explorer .button { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 24px; + height: 24px; + background: none; + border: none; + color: var(--ide-text-muted); + cursor: pointer; + font-size: 1rem; + padding: 0; + line-height: 1rem; +} + +file-explorer .button i { + height: 1rem; + width: 1rem; +} + + +file-explorer .button:hover { + color: var(--ide-text); + background: var(--ide-surface-muted); + border-radius: var(--ide-radius-xs); +} + +file-explorer .tree-view { + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + font-size: 0.86rem; + padding: 4px 6px; +} + +file-explorer.collapsed .tree-view { + display: none; +} + +file-explorer.collapsed .header { + padding: 0; + justify-content: center; +} + +file-explorer.collapsed .header > *:not(.collapse-button) { + display: none; +} + +file-explorer details { + margin: 2px 0; +} + +file-explorer summary { + cursor: pointer; + padding: 4px 8px; + border-radius: var(--ide-radius-xs); + user-select: none; + list-style: none; + display: flex; + align-items: center; + gap: 6px; +} + +file-explorer summary::-webkit-details-marker { + display: none; +} + +file-explorer summary:hover { + background: var(--ide-surface-subtle); +} + +file-explorer .folder-icon { + flex-shrink: 0; + font-size: 0.9rem; + color: var(--ide-text-muted); +} + +file-explorer .file-icon { + flex-shrink: 0; + font-size: 0.9rem; + color: var(--ide-text-muted); +} + +file-explorer .file-icon.icon-file-lock { + color: var(--ide-danger); +} + +file-explorer .file-item { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px 4px 8px; + border-radius: var(--ide-radius-xs); +} + +file-explorer .file-extname { + font-weight: 600; +} + +file-explorer .file-item:hover { + background: var(--ide-surface-subtle); +} + +file-explorer .file-item.open-in-editor .file-name { + font-variation-settings: 'wght' 550; +} + +file-explorer .file-name { + flex: 1; + cursor: pointer; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: inline-block; + position: relative; +} + +file-explorer .compiled-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: transparent; + border: 1px solid var(--ide-border-strong); + flex-shrink: 0; + margin-left: 4px; + opacity: 0.7; +} + +file-explorer .compiled-dot.needs-compile { + background: var(--ide-warning); + border-color: var(--ide-warning); + box-shadow: 0 0 0 1px color-mix(in srgb, var(--ide-warning) 30%, transparent); + opacity: 1; +} + +file-explorer .compiled-dot.hidden { + display: none; +} + +file-explorer .file-name-text { + display: inline-block; + will-change: transform; +} + +file-explorer .file-name-text.scrolling { + animation: file-name-scroll var(--scroll-duration, 8s) ease-in-out infinite alternate; + text-overflow: clip; +} + +@keyframes file-name-scroll { + 0% { + transform: translateX(0); + } + 100% { + transform: translateX(calc(-1 * var(--scroll-distance, 0px))); + } +} + +file-explorer .mjs-button { + flex-shrink: 0; + padding: 1px 3px; + font-size: 0.75rem; + font-weight: 400; + border: 1px solid color-mix(in srgb, var(--ide-accent) 35%, var(--ide-border)); + background: var(--ide-accent-soft); + color: var(--ide-accent-strong); + border-radius: var(--ide-radius-xs); + cursor: pointer; + line-height: 1.2; + transition: all 0.15s ease; +} + +file-explorer .mjs-button:hover { + background: color-mix(in srgb, var(--ide-accent-soft) 70%, white); + border-color: var(--ide-accent); + color: var(--ide-accent-strong); +} + +file-explorer .mjs-button:active { + background: color-mix(in srgb, var(--ide-accent-soft) 60%, var(--ide-accent)); + transform: translateY(1px); +} + +file-explorer .new-file-input { + flex: 1; + min-width: 0; + background: transparent; + border: none; + padding: 0; + font-size: inherit; + line-height: inherit; + color: var(--ide-text); + outline: none; + font-family: inherit; + caret-color: var(--ide-text); +} + +file-explorer .new-file-input:focus { + box-shadow: 0 1px 0 var(--ide-accent); +} + +file-explorer .new-file-input::placeholder { + color: var(--ide-text-subtle); +} + +search-panel, +source-control-panel, +outline-panel, +examples-panel { + display: flex; + flex-direction: column; + background: var(--ide-surface); + border-right: 1px solid var(--ide-border); + min-width: 0; + min-height: 0; + overflow: hidden; +} + +.mock-panel { + display: flex; + flex-direction: column; + min-width: 0; + min-height: 0; + height: 100%; +} + +.search-panel-shell { + display: flex; + flex-direction: column; + min-width: 0; + min-height: 0; + height: 100%; +} + +.outline-panel-shell { + display: flex; + flex-direction: column; + min-width: 0; + min-height: 0; + height: 100%; +} + +.search-panel-header { + display: flex; + align-items: center; + min-height: var(--panel-header-height); + padding: 6px 12px; + background: var(--ide-surface-subtle); + border-bottom: 1px solid var(--ide-border); +} + +.search-panel-header h2 { + font-size: 14px; + font-weight: 600; + line-height: 1.1; +} + +.mock-panel-header { + display: flex; + flex-direction: column; + justify-content: center; + gap: 2px; + min-height: var(--panel-header-height); + padding: 6px 12px; + background: var(--ide-surface-subtle); + border-bottom: 1px solid var(--ide-border); +} + +.mock-panel-header h2 { + font-size: 14px; + font-weight: 600; + line-height: 1.1; +} + +.mock-panel-header span { + color: var(--ide-text-muted); + font-size: 0.72rem; + line-height: 1.1; +} + +.outline-panel-header { + display: flex; + flex-direction: column; + justify-content: center; + gap: 2px; + min-height: var(--panel-header-height); + padding: 6px 12px; + background: var(--ide-surface-subtle); + border-bottom: 1px solid var(--ide-border); +} + +.outline-panel-header h2 { + font-size: 14px; + font-weight: 600; + line-height: 1.1; +} + +.outline-panel-header span { + color: var(--ide-text-muted); + font-size: 0.72rem; + line-height: 1.1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.mock-panel-body { + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + padding: 8px; +} + +.search-results { + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + padding: 8px; +} + +.outline-tree { + --outline-indent-step: 10px; + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + padding: 8px 6px; +} + +.outline-empty { + color: var(--ide-text-muted); + font-size: 0.82rem; + padding: 8px 6px; +} + +.outline-node { + display: block; + --outline-symbol: var(--ide-accent); + --outline-symbol-strong: var(--ide-accent-strong); +} + +.outline-node summary { + list-style: none; +} + +.outline-node summary::-webkit-details-marker { + display: none; +} + +.outline-node-summary, +.outline-node-leaf { + width: 100%; + min-height: 28px; + box-sizing: border-box; + display: grid; + grid-template-columns: 14px max-content 16px minmax(0, 1fr) max-content; + align-items: center; + column-gap: 4px; + border: 1px solid transparent; + border-radius: var(--ide-radius-sm); + background: transparent; + color: var(--ide-text); + cursor: pointer; + font: inherit; + padding: 4px 6px 4px calc(var(--outline-depth, 0) * var(--outline-indent-step)); + text-align: left; +} + +.outline-node-leaf { + --outline-symbol: var(--ide-accent); + --outline-symbol-strong: var(--ide-accent-strong); +} + +.outline-kind-file { + --outline-symbol: #5f6b6f; + --outline-symbol-strong: #435056; +} + +.outline-kind-module { + --outline-symbol: var(--ide-accent); + --outline-symbol-strong: var(--ide-accent-strong); +} + +.outline-kind-class { + --outline-symbol: var(--ide-internal); + --outline-symbol-strong: #5b3a70; +} + +.outline-kind-object { + --outline-symbol: var(--ide-info); + --outline-symbol-strong: #0e4f80; +} + +.outline-kind-function { + --outline-symbol: var(--ide-success); + --outline-symbol-strong: #235d34; +} + +.outline-kind-type { + --outline-symbol: #6f5fb7; + --outline-symbol-strong: #514289; +} + +.outline-kind-pattern { + --outline-symbol: #6b7280; + --outline-symbol-strong: #4b5563; +} + +.outline-kind-value { + --outline-symbol: var(--ide-warning); + --outline-symbol-strong: #884000; +} + +.outline-kind-mutable-value { + --outline-symbol: var(--ide-danger); + --outline-symbol-strong: #9f2529; +} + +.outline-kind-parameter { + --outline-symbol: #60758f; + --outline-symbol-strong: #46566b; +} + +.outline-kind-constructor { + --outline-symbol: #9a4d6d; + --outline-symbol-strong: #743852; +} + +.outline-kind-symbol { + --outline-symbol: var(--ide-text-muted); + --outline-symbol-strong: var(--ide-text); +} + +.outline-node-caret, +.outline-node-spacer { + width: 14px; + height: 18px; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.outline-node-caret { + border-radius: var(--ide-radius-xs); + cursor: pointer; +} + +.outline-node-caret::before { + content: ""; + width: 0; + height: 0; + border-top: 4px solid transparent; + border-bottom: 4px solid transparent; + border-left: 5px solid var(--ide-text-muted); + transform: rotate(0deg); + transition: transform 120ms ease; +} + +.outline-node-caret:hover { + background: var(--ide-surface-muted); +} + +.outline-node-caret:focus-visible { + outline: none; + background: var(--ide-accent-soft); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--ide-accent) 24%, transparent); +} + +.outline-node[open] > .outline-node-summary .outline-node-caret::before { + transform: rotate(90deg); +} + +.outline-node-summary:hover, +.outline-node-leaf:hover { + background: var(--ide-surface-subtle); +} + +.outline-node.selected > .outline-node-summary, +.outline-node-leaf.selected { + background: var(--ide-accent-soft); + border-color: color-mix(in srgb, var(--ide-accent) 45%, var(--ide-border)); +} + +.outline-node-kind { + display: inline-flex; + align-items: center; + justify-content: center; + justify-self: start; + min-width: 0; + border: 1px solid color-mix(in srgb, var(--outline-symbol) 38%, var(--ide-border)); + border-radius: var(--ide-radius-sm); + background: color-mix(in srgb, var(--outline-symbol) 12%, var(--ide-surface)); + color: var(--outline-symbol-strong); + font-size: 0.68rem; + font-weight: 700; + line-height: 1; + min-height: 17px; + overflow: hidden; + padding: 1px 2px; + text-align: center; +} + +.outline-node-icon { + color: var(--outline-symbol); + font-size: 0.86rem; + margin-left: 3px; +} + +.outline-node-name { + font-family: var(--monospace); + font-size: 0.78rem; + letter-spacing: 0; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + line-height: 1.2; +} + +.outline-node-location { + color: var(--ide-text-muted); + font-size: 0.72rem; + line-height: 1.2; + white-space: nowrap; +} + +.outline-node-children { + display: grid; + gap: 1px; + position: relative; +} + +.outline-node-children::before { + content: ""; + position: absolute; + top: 2px; + bottom: 4px; + left: calc(7px + (var(--outline-depth, 0) * var(--outline-indent-step))); + border-left: 1px solid color-mix(in srgb, var(--ide-border) 70%, transparent); + pointer-events: none; +} + +.outline-node:not([open]) > .outline-node-children { + display: none; +} + +.search-row { + display: flex; + gap: 6px; + padding: 8px; + border-bottom: 1px solid var(--ide-border); + background: var(--ide-surface); +} + +.search-input { + min-width: 0; + flex: 1; + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + background: var(--ide-surface); + color: var(--ide-text); + font: inherit; + font-size: 0.85rem; + padding: 5px 7px; +} + +.search-input:focus { + outline: none; + border-color: var(--ide-accent); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--ide-accent) 24%, transparent); +} + +.search-input::-webkit-search-decoration, +.search-input::-webkit-search-cancel-button, +.search-input::-webkit-search-results-button, +.search-input::-webkit-search-results-decoration { + display: none; + appearance: none; + -webkit-appearance: none; +} + +.search-clear-button, +.mock-icon-button { + width: 28px; + height: 28px; + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + background: var(--ide-surface); + color: var(--ide-text-muted); + cursor: pointer; + line-height: 1; +} + +.search-clear-button:hover, +.mock-icon-button:hover { + background: var(--ide-surface-subtle); + border-color: var(--ide-border-strong); + color: var(--ide-text); +} + +.mock-outline-row, +.mock-example-row { + width: 100%; + display: grid; + gap: 3px; + text-align: left; + border: 1px solid transparent; + border-radius: var(--ide-radius-sm); + background: transparent; + color: var(--ide-text); + cursor: pointer; + padding: 7px 8px; +} + +.mock-outline-row:hover, +.mock-example-row:hover { + background: var(--ide-surface-subtle); +} + +.mock-outline-row + .mock-outline-row, +.mock-example-row + .mock-example-row, +.mock-change-row + .mock-change-row { + margin-top: 4px; +} + +.mock-outline-name, +.mock-example-title { + font-weight: 600; + line-height: 1.2; +} + +.mock-change-summary, +.mock-example-file, +.mock-outline-line, +.mock-example-description { + color: var(--ide-text-muted); + font-size: 0.78rem; + line-height: 1.25; +} + +.search-summary { + position: sticky; + top: -8px; + z-index: 1; + margin: -8px -8px 8px; + border-bottom: 1px solid var(--ide-border); + background: var(--ide-surface); + color: var(--ide-text-muted); + font-size: 0.78rem; + font-weight: 600; + padding: 7px 8px; +} + +.search-group + .search-group { + margin-top: 10px; +} + +.search-group-heading { + display: flex; + align-items: baseline; + gap: 6px; + width: 100%; + min-width: 0; + border: 1px solid transparent; + border-radius: var(--ide-radius-sm); + background: transparent; + color: var(--ide-text); + cursor: pointer; + padding: 3px 2px 5px; + text-align: left; + overflow: hidden; +} + +.search-group-heading:hover { + background: var(--ide-surface-subtle); +} + +.search-group-caret { + flex: 0 0 auto; + color: var(--ide-text-muted); + font-size: 0.75rem; + transition: transform 0.12s ease; +} + +.search-group:not(.collapsed) .search-group-caret { + transform: rotate(90deg); +} + +.search-group-name { + min-width: 0; + flex: 0 1 auto; + overflow: hidden; + color: var(--ide-text); + font-size: 0.82rem; + font-weight: 700; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.search-group-path { + min-width: 0; + flex: 1 1 auto; + overflow: hidden; + color: var(--ide-text-muted); + font-size: 0.74rem; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.search-group-count { + min-width: 20px; + flex: 0 0 auto; + border: 1px solid var(--ide-border); + border-radius: 999px; + background: var(--ide-surface-subtle); + color: var(--ide-text-muted); + font-size: 0.72rem; + font-weight: 700; + line-height: 1.1; + padding: 1px 6px; + text-align: center; +} + +.search-result-row { + width: 100%; + display: grid; + grid-template-columns: 30px minmax(0, 1fr); + gap: 8px; + align-items: baseline; + min-width: 0; + overflow: hidden; + text-align: left; + border: 1px solid transparent; + border-radius: var(--ide-radius-sm); + background: transparent; + color: var(--ide-text); + cursor: pointer; + padding: 5px 7px; +} + +.search-result-row:hover { + background: var(--ide-surface-subtle); +} + +.search-result-row[hidden], +.search-group.collapsed .search-group-results { + display: none; +} + +.search-result-row + .search-result-row { + margin-top: 2px; +} + +.search-result-line { + min-width: 0; + overflow: hidden; + color: var(--ide-text-muted); + font-family: var(--monospace); + font-size: 0.74rem; + line-height: 1.35; + text-align: right; +} + +.search-result-excerpt { + display: block; + min-width: 0; + max-width: 100%; + overflow: hidden; + color: var(--ide-text); + font-size: 0.78rem; + line-height: 1.35; + text-overflow: ellipsis; + white-space: nowrap; +} + +.search-result-excerpt mark { + border-radius: 2px; + background: color-mix(in srgb, var(--ide-accent) 28%, transparent); + color: var(--ide-text); + font-weight: 700; + padding: 0 1px; +} + +.search-result-excerpt, +.mock-example-preview { + font-family: var(--monospace); +} + +.mock-scm-section + .mock-scm-section, +.mock-example-detail { + margin-top: 10px; +} + +.mock-section-title { + display: flex; + align-items: center; + justify-content: space-between; + color: var(--ide-text-muted); + font-size: 0.75rem; + font-weight: 700; + padding: 4px 2px 6px; + text-transform: uppercase; +} + +.mock-count { + min-width: 20px; + border: 1px solid var(--ide-border); + border-radius: 999px; + background: var(--ide-surface-subtle); + color: var(--ide-text-muted); + text-align: center; + padding: 1px 6px; +} + +.mock-change-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto; + align-items: center; + gap: 8px; + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + background: var(--ide-surface); + padding: 7px 7px 7px 9px; +} + +.mock-change-main { + min-width: 0; + display: grid; + gap: 2px; +} + +.mock-change-path { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: 600; +} + +.mock-change-status, +.mock-outline-kind { + border: 1px solid color-mix(in srgb, var(--ide-accent) 35%, var(--ide-border)); + border-radius: 999px; + background: var(--ide-accent-soft); + color: var(--ide-accent-strong); + font-size: 0.7rem; + font-weight: 700; + padding: 1px 6px; + white-space: nowrap; +} + +.mock-outline-row { + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; +} + +.mock-outline-row.active, +.mock-example-row.active { + background: var(--ide-accent-soft); + border-color: color-mix(in srgb, var(--ide-accent) 45%, var(--ide-border)); +} + +.mock-panel-feedback { + margin-top: 10px; + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + background: var(--ide-surface-subtle); + color: var(--ide-text-muted); + font-size: 0.8rem; + line-height: 1.35; + padding: 8px; +} + +.mock-example-detail { + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + background: var(--ide-surface-subtle); + padding: 9px; +} + +.mock-example-detail-title { + font-weight: 700; + margin-bottom: 5px; +} + +.mock-example-preview { + margin-top: 8px; + padding: 8px; + overflow-x: auto; + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-xs); + background: var(--ide-surface); + color: var(--ide-text); + font-size: 0.78rem; + line-height: 1.35; +} + +.mock-empty, +.search-empty { + color: var(--ide-text-subtle); + font-size: 0.82rem; + padding: 8px; +} + +/* Editor Workbench */ +editor-workbench { + grid-column: 3; + display: flex; + flex-direction: column; + min-width: 0; + min-height: 0; + background: var(--ide-editor-bg); + overflow: hidden; +} + +editor-workbench .editor-workbench-shell { + display: flex; + flex-direction: column; + min-width: 0; + min-height: 0; + height: 100%; +} + +editor-workbench .editor-chrome-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + min-height: 34px; + padding: 5px 8px 5px 12px; + background: var(--ide-surface); + border-bottom: 1px solid var(--ide-border); +} + +editor-workbench .editor-breadcrumbs { + display: inline-flex; + align-items: center; + gap: 5px; + min-width: 0; + overflow: hidden; + color: var(--ide-text-muted); + font-size: 0.78rem; + line-height: 1; +} + +editor-workbench .editor-breadcrumbs i { + width: 0.78rem; + height: 0.78rem; + color: var(--ide-text-subtle); +} + +editor-workbench .editor-breadcrumb-root, +editor-workbench .editor-breadcrumb-segment { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +editor-workbench .editor-breadcrumb-root { + flex: 0 0 auto; +} + +editor-workbench .editor-breadcrumb-segment { + flex: 0 1 auto; +} + +editor-workbench .editor-breadcrumb-active { + color: var(--ide-text); +} + +editor-workbench .editor-chrome-actions { + display: flex; + align-items: center; + gap: 4px; + flex: 0 0 auto; +} + +editor-workbench .editor-action-button, +editor-workbench .compiled-close-button, +editor-workbench .compiled-copy-button, +editor-workbench .compiled-download-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 5px; + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + background: var(--ide-surface); + color: var(--ide-text-muted); + cursor: pointer; + font-size: 0.78rem; + line-height: 1; + min-height: 26px; + padding: 5px 8px; +} + +editor-workbench .editor-action-button:hover, +editor-workbench .compiled-close-button:hover, +editor-workbench .compiled-copy-button:hover, +editor-workbench .compiled-download-button:hover { + background: var(--ide-accent-soft); + border-color: color-mix(in srgb, var(--ide-accent) 38%, var(--ide-border)); + color: var(--ide-accent-strong); +} + +editor-workbench .editor-action-button i, +editor-workbench .compiled-close-button i, +editor-workbench .compiled-copy-button i, +editor-workbench .compiled-download-button i { + width: 0.86rem; + height: 0.86rem; +} + +editor-workbench .editor-split-shell { + flex: 1; + min-width: 0; + min-height: 0; + display: grid; + grid-template-columns: minmax(0, 1fr); + overflow: hidden; +} + +editor-workbench.compiled-open .editor-split-shell { + grid-template-columns: minmax(0, 1fr) minmax(320px, 380px); +} + +editor-workbench editor-panel { + grid-column: auto; + min-width: 0; + min-height: 0; +} + +editor-workbench .compiled-output-pane { + display: flex; + flex-direction: column; + min-width: 0; + min-height: 0; + border-left: 1px solid var(--ide-border); + background: var(--ide-surface); + overflow: hidden; +} + +editor-workbench .compiled-output-pane[hidden] { + display: none; +} + +editor-workbench .compiled-output-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 8px 10px; + background: var(--ide-surface-subtle); + border-bottom: 1px solid var(--ide-border); +} + +editor-workbench .compiled-output-header h2 { + color: var(--ide-text); + font-size: 0.9rem; + font-weight: 700; + line-height: 1.2; +} + +editor-workbench .compiled-output-file { + display: block; + max-width: 270px; + overflow: hidden; + color: var(--ide-text-muted); + font-family: var(--monospace); + font-size: 0.68rem; + line-height: 1.25; + text-overflow: ellipsis; + white-space: nowrap; +} + +editor-workbench .compiled-close-button { + width: 28px; + padding: 0; + flex: 0 0 auto; +} + +editor-workbench .compiled-targets { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 5px; + padding: 8px 10px; + border-bottom: 1px solid var(--ide-border); +} + +editor-workbench .compiled-target-option { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 5px; + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + background: var(--ide-surface); + color: var(--ide-text-muted); + cursor: pointer; + font-size: 0.78rem; + line-height: 1; + min-height: 28px; + padding: 5px 7px; +} + +editor-workbench .compiled-target-option:has(input:checked) { + background: var(--ide-accent-soft); + border-color: color-mix(in srgb, var(--ide-accent) 38%, var(--ide-border)); + color: var(--ide-accent-strong); +} + +editor-workbench .compiled-target-option input { + accent-color: var(--ide-accent); +} + +editor-workbench .compiled-output-actions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 6px; + padding: 8px 10px; + border-bottom: 1px solid var(--ide-border); +} + +editor-workbench .compiled-output-code { + flex: 1; + min-height: 0; + margin: 0; + overflow: auto; + padding: 10px; + background: var(--ide-editor-bg); + color: var(--ide-text); + font-family: var(--monospace); + font-size: 0.78rem; + line-height: 1.45; + white-space: pre; +} + +editor-workbench .compiled-output-feedback { + border-top: 1px solid var(--ide-border); + background: var(--ide-accent-soft); + color: var(--ide-accent-strong); + font-size: 0.76rem; + line-height: 1.25; + padding: 7px 10px; +} + +editor-workbench .compiled-output-feedback[hidden] { + display: none; +} + +editor-workbench editor-panel .tab-bar-wrapper { + height: 36px; +} + +editor-workbench editor-panel .tab { + min-width: 112px; + padding-top: 0.42rem; + padding-bottom: 0.42rem; +} + +/* Editor Panel */ +editor-panel { + grid-column: 3; + display: flex; + flex-direction: column; + background: var(--ide-editor-bg); + overflow: hidden; + border-left: none; + border-right: none; + min-width: 0; + min-height: 0; +} + +editor-panel .tab-bar-wrapper { + position: relative; + display: flex; + background: var(--ide-surface-subtle); + border-bottom: 1px solid var(--ide-border); + height: var(--panel-header-height); +} + +editor-panel .tab-bar { + display: flex; + flex: 1; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scrollbar-width: none; + -ms-overflow-style: none; +} + +editor-panel .tab-bar::-webkit-scrollbar { + display: none; +} + +editor-panel .tab-scroll-arrow { + display: none; + position: absolute; + top: 0; + align-items: center; + justify-content: center; + width: 40px; + height: 100%; + background: linear-gradient(to right, var(--ide-surface-subtle), transparent); + border: none; + color: var(--ide-text-muted); + cursor: pointer; + font-size: 20px; + font-weight: bold; + padding: 0; + z-index: 1; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s ease; +} + +editor-panel .tab-scroll-arrow.visible { + opacity: 1; + pointer-events: auto; +} + +editor-panel .tab-scroll-arrow:hover { + color: var(--ide-text); +} + +editor-panel .tab-scroll-left { + left: 0; + background: linear-gradient(to right, var(--ide-surface-subtle) 75%, transparent); +} + +editor-panel .tab-scroll-right { + right: 0; + background: linear-gradient(to left, var(--ide-surface-subtle) 75%, transparent); +} + +editor-panel .tab { + display: flex; + align-items: center; + gap: 0.4rem; + padding: 0.5rem 0.5rem 0.5rem 0.75rem; + background: var(--ide-surface-subtle); + border-right: 1px solid var(--ide-border); + cursor: pointer; + font-size: 0.875rem; + white-space: nowrap; + color: var(--ide-text-muted); + user-select: none; + transition: color 0.15s ease, background 0.15s ease; + flex: 0 0 auto; + min-width: 120px; + max-width: 260px; +} + +editor-panel .tab.active { + background: var(--ide-editor-bg); + color: var(--ide-text); + box-shadow: inset 0 -2px 0 var(--ide-accent); +} + +editor-panel .tab:not(.active):hover { + background: var(--ide-surface-muted); + color: var(--ide-text); +} + +editor-panel .tab-name { + display: flex; + align-items: center; + gap: 0; + min-width: 0; + overflow: hidden; + flex: 1 1 auto; +} + +editor-panel .tab-name > .tab-basename { + font-variation-settings: 'wght' 450; + transition: font-variation-settings 0.60s ease; +} + +editor-panel .tab-name-text { + display: inline-block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; + will-change: transform; + padding-right: 0.25rem; + padding-left: 0.1rem; +} + +editor-panel .tab-name-text.scrolling { + animation: file-name-scroll var(--scroll-duration, 8s) ease-in-out infinite alternate; + text-overflow: clip; +} + +editor-panel .tab-lock { + margin-right: 0.35rem; + color: inherit; + font-size: 0.9rem; +} + +editor-panel .tab.active .tab-name > .tab-basename { + font-variation-settings: 'wght' 650; +} + +editor-panel .tab-extension { + font-weight: 600; + color: var(--ide-text-muted); +} + +editor-panel .tab-close { + background: none; + border: none; + color: var(--ide-text-muted); + cursor: pointer; + padding: 0.25rem; + font-size: 1rem; + line-height: 1; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s ease; + flex-shrink: 0; +} + +editor-panel .tab-close:hover { + color: var(--ide-text); + background: var(--ide-surface-muted); +} + +file-tooltip { + font-size: 0.875rem; + color: var(--ide-text-muted); + position: fixed; + width: max-content; + max-width: 400px; + top: 0; + left: 0; + background: var(--ide-surface); + border: 1px solid var(--ide-border); + padding: 0.25rem 0.5rem; + border-radius: var(--ide-radius-sm); + box-shadow: var(--ide-shadow-md); + z-index: 2000; + word-break: break-all; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s ease 0s; + --tooltip-bg: var(--ide-surface); + --tooltip-border: var(--ide-border); + --arrow-size: 8px; + --arrow-border-size: 10px; +} + +file-tooltip.visible { + opacity: 0.9; + pointer-events: auto; +} + +file-tooltip::before, +file-tooltip::after { + content: ""; + position: absolute; + width: 0; + height: 0; +} + +file-tooltip[data-placement="right"]::before { + border-style: solid; + border-width: var(--arrow-border-size) var(--arrow-border-size) var(--arrow-border-size) 0; + border-color: transparent var(--tooltip-border) transparent transparent; + left: calc(-1 * var(--arrow-border-size)); + top: 50%; + transform: translateY(-50%); +} + +file-tooltip[data-placement="right"]::after { + border-style: solid; + border-width: var(--arrow-size) var(--arrow-size) var(--arrow-size) 0; + border-color: transparent var(--tooltip-bg) transparent transparent; + left: calc(-1 * var(--arrow-size)); + top: 50%; + transform: translateY(-50%); +} + +file-tooltip[data-placement="bottom"]::before { + border-style: solid; + border-width: 0 var(--arrow-border-size) var(--arrow-border-size) var(--arrow-border-size); + border-color: transparent transparent var(--tooltip-border) transparent; + top: calc(-1 * var(--arrow-border-size)); + left: 50%; + transform: translateX(-50%); +} + +file-tooltip[data-placement="bottom"]::after { + border-style: solid; + border-width: 0 var(--arrow-size) var(--arrow-size) var(--arrow-size); + border-color: transparent transparent var(--tooltip-bg) transparent; + top: calc(-1 * var(--arrow-size)); + left: 50%; + transform: translateX(-50%); +} + +file-tooltip .tooltip-grid { + display: grid; + grid-template-columns: auto 1fr; + gap: 0.2rem 0.75rem; + align-items: start; +} + +file-tooltip .tooltip-label { + color: var(--ide-text-subtle); + font-weight: 600; + white-space: nowrap; +} + +file-tooltip .tooltip-value { + color: var(--ide-text); + word-break: break-word; + min-width: 0; +} + +editor-panel .editor-container { + flex: 1; + min-height: 0; + position: relative; + overflow: hidden; +} + +editor-panel .editor-codemirror { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: none; + overflow: hidden; +} + +editor-panel .editor-codemirror.active { + display: block; +} + +/* CodeMirror customization */ +editor-panel .cm-editor { + height: 100%; + background: var(--ide-editor-bg); + color: var(--ide-text); +} + +editor-panel .cm-gutters { + background-color: #f4f5f2 !important; + background-image: radial-gradient(circle, color-mix(in srgb, var(--ide-border-strong) 36%, transparent) 0.65px, transparent 0.75px) !important; + background-size: 6px 6px !important; + color: var(--ide-text-subtle); + border-right: none !important; + font-family: var(--editor-font-family); + font-variation-settings: var(--editor-font-variation-normal); + font-size: var(--editor-font-size); + padding-left: 0; +} + +editor-panel .cm-content { + font-family: var(--editor-font-family); + font-variation-settings: var(--editor-font-variation-normal); + font-variant-ligatures: var(--editor-font-ligatures); + letter-spacing: var(--editor-letter-spacing); + line-height: var(--editor-line-height); + background-image: var(--editor-rulers-background); + background-repeat: repeat-y; +} + +editor-panel .cm-content :is(b, strong), +editor-panel .cm-content [style*="font-weight"] { + font-variation-settings: var(--editor-font-variation-bold); +} + +editor-panel .cm-content :is(i, em), +editor-panel .cm-content [style*="font-style: italic"] { + font-variation-settings: var(--editor-font-variation-italic); +} + +editor-panel .cm-content :is(b, strong) :is(i, em), +editor-panel .cm-content :is(i, em) :is(b, strong), +editor-panel .cm-content [style*="font-weight"][style*="font-style: italic"] { + font-variation-settings: var(--editor-font-variation-bold-italic); +} + +editor-panel .cm-gutter, +editor-panel .cm-lineNumbers, +editor-panel .cm-foldGutter { + border-right: none !important; +} + +editor-panel .cm-lineNumbers .cm-gutterElement { + font-size: var(--editor-font-size); + min-width: 2.6ch !important; + padding-left: 14px !important; + padding-right: 4px !important; + text-align: right !important; +} + +editor-panel .cm-foldGutter .cm-gutterElement { + min-width: 10px !important; + padding: 0 !important; + color: var(--ide-text-subtle); +} + +editor-panel .cm-foldGutter .cm-gutterElement:hover { + color: var(--ide-accent-strong); +} + +editor-panel .cm-activeLineGutter { + background-color: color-mix(in srgb, var(--ide-surface-muted) 62%, transparent) !important; +} + +editor-panel .cm-line.mls-indent-guides { + --mls-indent-guide-color: #dfe3de; + --mls-indent-guide-left: 0px; + --mls-indent-guide-width: 0ch; + position: relative; +} + +editor-panel .cm-line.mls-indent-guides::before { + background-image: repeating-linear-gradient( + to right, + var(--mls-indent-guide-color) 0, + var(--mls-indent-guide-color) 1px, + transparent 1px, + transparent 2ch + ); + background-position: left top; + background-repeat: repeat-x; + background-size: 2ch 100%; + bottom: -1px; + content: ""; + left: var(--mls-indent-guide-left); + pointer-events: none; + position: absolute; + top: -1px; + width: calc(var(--mls-indent-guide-width) + 1px); +} + +editor-panel .cm-panels.cm-panels-bottom { + position: absolute; + left: 50%; + transform: translateX(-50%); + bottom: 12px; + width: min(900px, calc(100% - 24px)); + background: color-mix(in srgb, var(--ide-surface) 94%, transparent); + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + box-shadow: var(--ide-shadow-md); + padding: 0.5rem 0.75rem; + z-index: 1100; +} + +editor-panel .cm-search.cm-panel { + position: relative; + display: grid; + grid-template-columns: minmax(280px, 1fr) repeat(3, auto) repeat(3, auto); + grid-template-rows: auto auto; + column-gap: 0.5rem; + row-gap: 0.4rem; + align-items: center; + font-size: 1rem; + color: var(--ide-text); +} + +editor-panel .cm-search .cm-textfield { + border: 1px solid var(--ide-border); + background: var(--ide-surface); + color: var(--ide-text); + border-radius: var(--ide-radius-sm); + padding: 0.25rem 0.5rem; + min-width: 180px; + font-size: 1.02rem; + line-height: 1.25rem; + font-family: var(--monospace); + transition: border-color 0.15s ease, box-shadow 0.15s ease; +} + +editor-panel .cm-search .cm-textfield:focus { + outline: none; + border-color: var(--ide-accent); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--ide-accent) 25%, transparent); +} + +editor-panel .cm-search label { + display: inline-flex; + align-items: center; + gap: 0.35rem; + color: var(--ide-text-muted); + font-size: 0.9rem; + white-space: nowrap; +} + +editor-panel .cm-search label input[type="checkbox"] { + accent-color: var(--ide-accent); +} + +editor-panel .cm-search .cm-button, +editor-panel .cm-search button[name="close"] { + background: linear-gradient(180deg, var(--ide-surface), var(--ide-surface-subtle)); + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + color: var(--ide-text); + padding: 0.25rem 0.55rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s ease, border-color 0.15s ease, transform 0.1s ease; +} + +editor-panel .cm-search .cm-button:hover, +editor-panel .cm-search button[name="close"]:hover { + background: linear-gradient(180deg, var(--ide-surface-subtle), var(--ide-surface-muted)); + border-color: var(--ide-border-strong); +} + +editor-panel .cm-search .cm-button:active, +editor-panel .cm-search button[name="close"]:active { + transform: translateY(1px); +} + +editor-panel .cm-search button[name="close"] { + position: absolute; + top: 6px; + right: 6px; + font-size: 1.05rem; + padding: 4px 8px; +} + +editor-panel .cm-search br { + display: none; +} + +/* Grid placement for search panel */ +editor-panel .cm-search .cm-textfield[name="search"] { + grid-row: 1; + grid-column: 1; +} + +editor-panel .cm-search label:nth-of-type(1) { + grid-row: 1; + grid-column: 2; +} + +editor-panel .cm-search label:nth-of-type(2) { + grid-row: 1; + grid-column: 3; +} + +editor-panel .cm-search label:nth-of-type(3) { + grid-row: 1; + grid-column: 4; +} + +editor-panel .cm-search .cm-button[name="next"] { + grid-row: 1; + grid-column: 5; +} + +editor-panel .cm-search .cm-button[name="prev"] { + grid-row: 1; + grid-column: 6; +} + +editor-panel .cm-search .cm-button[name="select"] { + grid-row: 1; + grid-column: 7; +} + +editor-panel .cm-search .cm-textfield[name="replace"] { + grid-row: 2; + grid-column: 1; +} + +editor-panel .cm-search .cm-button[name="replace"] { + grid-row: 2; + grid-column: 5; +} + +editor-panel .cm-search .cm-button[name="replaceAll"] { + grid-row: 2; + grid-column: 6; +} + + +editor-panel .empty-state { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: var(--ide-text-muted); + font-size: 14px; +} + +editor-panel .empty-state.hidden { + display: none; +} + +/* Problems Inspector */ +diagnostics-inspector { + container-type: inline-size; + display: flex; + flex-direction: column; + background: var(--ide-surface); + border-left: 1px solid var(--ide-border); + position: relative; + min-width: 0; + min-height: 0; + overflow: hidden; +} + +diagnostics-inspector .diagnostics-header { + display: flex; + flex-direction: column; + gap: 8px; + padding: 8px 10px; + background: var(--ide-surface-subtle); + border-bottom: 1px solid var(--ide-border); + min-height: 112px; +} + +diagnostics-inspector .diagnostics-title-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +diagnostics-inspector h2 { + font-size: 14px; + font-weight: 600; + line-height: 1.2; +} + +diagnostics-inspector .diagnostics-counts { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 4px; +} + +diagnostics-inspector .diagnostics-count { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + background: var(--ide-surface); + padding: 5px 6px; + color: var(--ide-text-muted); + cursor: pointer; + font: inherit; + text-align: left; +} + +diagnostics-inspector .diagnostics-count:hover:not(.active) { + background: var(--ide-surface-subtle); + border-color: var(--ide-border-strong); + color: var(--ide-text); +} + +diagnostics-inspector .diagnostics-count.active { + background: var(--ide-accent-soft); + border-color: color-mix(in srgb, var(--ide-accent) 56%, var(--ide-border)); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ide-accent) 34%, transparent); + color: var(--ide-accent-strong); +} + +diagnostics-inspector .diagnostics-count.active:hover { + background: color-mix(in srgb, var(--ide-accent-soft) 82%, var(--ide-surface)); + border-color: color-mix(in srgb, var(--ide-accent) 72%, var(--ide-border)); +} + +diagnostics-inspector .diagnostics-count span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 0.72rem; + font-weight: 600; + line-height: 1.1; +} + +diagnostics-inspector .diagnostics-count strong { + color: var(--ide-text); + font-size: 0.86rem; + line-height: 1; +} + +diagnostics-inspector .diagnostics-count-error strong { + color: var(--ide-danger); +} + +diagnostics-inspector .diagnostics-count-warning strong { + color: var(--ide-warning); +} + +diagnostics-inspector .diagnostics-count-internal strong { + color: var(--ide-internal); +} + +diagnostics-inspector .diagnostics-count-info strong { + color: var(--ide-info); +} + +diagnostics-inspector .diagnostics-mode-tabs { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 4px; +} + +diagnostics-inspector .diagnostics-mode-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + height: 28px; + min-width: 0; + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + background: var(--ide-surface); + color: var(--ide-text-muted); + cursor: pointer; + font-size: 0.74rem; + line-height: 1; + padding: 0 4px; +} + +diagnostics-inspector .diagnostics-mode-button:hover:not(.active) { + background: var(--ide-surface-subtle); + border-color: var(--ide-border-strong); + color: var(--ide-text); +} + +diagnostics-inspector .diagnostics-mode-button.active { + background: var(--ide-accent-soft); + border-color: color-mix(in srgb, var(--ide-accent) 56%, var(--ide-border)); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ide-accent) 34%, transparent); + color: var(--ide-accent-strong); +} + +diagnostics-inspector .diagnostics-mode-button.active:hover { + background: color-mix(in srgb, var(--ide-accent-soft) 82%, var(--ide-surface)); + border-color: color-mix(in srgb, var(--ide-accent) 72%, var(--ide-border)); +} + +diagnostics-inspector .diagnostics-mode-button i { + width: 0.86rem; + height: 0.86rem; +} + +diagnostics-inspector .diagnostics-mode-button span { + min-width: 0; + overflow: hidden; + line-height: 1; + text-overflow: ellipsis; + white-space: nowrap; +} + +diagnostics-inspector .diagnostics-content { + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + padding: 8px; +} + +diagnostics-inspector .diagnostics-empty { + min-height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; + color: var(--ide-text-subtle); + font-size: 0.88rem; + font-weight: 500; +} + +diagnostics-inspector .diagnostics-empty i { + color: color-mix(in srgb, var(--ide-success) 46%, var(--ide-text-subtle)); + font-size: 2.5rem; + opacity: 0.55; +} + +diagnostics-inspector .diagnostics-list, +diagnostics-inspector .diagnostics-source-cards { + display: flex; + flex-direction: column; + gap: 6px; +} + +diagnostics-inspector .diagnostics-row { + width: 100%; + min-width: 0; + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + background: var(--ide-surface); + color: var(--ide-text); + overflow: hidden; +} + +diagnostics-inspector .diagnostics-row-summary { + width: 100%; + min-width: 0; + display: flex; + align-items: flex-start; + gap: 8px; + border: 0; + background: transparent; + color: inherit; + cursor: pointer; + padding: 8px; + text-align: left; +} + +diagnostics-inspector .diagnostics-row:hover { + background: var(--ide-surface-subtle); + border-color: var(--ide-border-strong); +} + +diagnostics-inspector .diagnostics-row-main { + min-width: 0; +} + +diagnostics-inspector .diagnostics-row-main { + display: grid; + gap: 3px; + flex: 1; +} + +diagnostics-inspector .diagnostics-row-message { + min-width: 0; + color: var(--ide-text); + font-size: 0.82rem; + font-weight: 600; + line-height: 1.32; + white-space: normal; + overflow-wrap: anywhere; +} + +diagnostics-inspector .diagnostics-row-detail { + min-width: 0; + overflow: hidden; + color: var(--ide-text-muted); + font-family: var(--monospace); + font-size: 0.68rem; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +diagnostics-inspector .diagnostics-row-caret { + color: var(--ide-text-subtle); + flex: 0 0 auto; + width: 0.9rem; + height: 0.9rem; + transition: transform 120ms ease; +} + +diagnostics-inspector .diagnostics-row.expanded .diagnostics-row-caret { + transform: rotate(90deg); +} + +diagnostics-inspector .diagnostics-severity-icon { + flex: 0 0 auto; + width: 0.95rem; + height: 0.95rem; + margin-top: 1px; +} + +diagnostics-inspector .diagnostic-error .diagnostics-severity-icon { + color: var(--ide-danger); +} + +diagnostics-inspector .diagnostic-warning .diagnostics-severity-icon { + color: var(--ide-warning); +} + +diagnostics-inspector .diagnostic-internal .diagnostics-severity-icon { + color: var(--ide-internal); +} + +diagnostics-inspector .diagnostics-source-card { + display: grid; + gap: 8px; + border-top: 1px solid var(--ide-border); + background: color-mix(in srgb, var(--ide-surface-subtle) 62%, var(--ide-surface)); + padding: 9px; +} + +diagnostics-inspector .diagnostics-source-card-header { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 8px; + align-items: start; +} + +diagnostics-inspector .diagnostics-source-card h3 { + overflow: hidden; + color: var(--ide-text); + font-size: 0.86rem; + font-weight: 700; + line-height: 1.25; + text-overflow: ellipsis; + white-space: nowrap; +} + +diagnostics-inspector .diagnostics-source-card-header span { + display: block; + min-width: 0; + overflow: hidden; + color: var(--ide-text-muted); + font-family: var(--monospace); + font-size: 0.68rem; + line-height: 1.25; + text-overflow: ellipsis; + white-space: nowrap; +} + +diagnostics-inspector .diagnostics-source-preview { + display: grid; + grid-template-columns: 36px minmax(0, 1fr); + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-xs); + background: var(--ide-surface-subtle); + overflow: hidden; + font-family: var(--monospace); +} + +diagnostics-inspector .diagnostics-source-line-number { + display: flex; + align-items: flex-start; + justify-content: flex-end; + padding: 6px; + border-right: 1px solid var(--ide-border); + background: var(--ide-surface-muted); + color: var(--ide-text-subtle); + font-size: 0.72rem; + line-height: 1.4; +} + +diagnostics-inspector .diagnostics-source-preview pre { + min-width: 0; + margin: 0; + overflow-x: auto; + padding: 6px 8px; + color: var(--ide-text); + font-family: var(--monospace); + font-size: 0.76rem; + line-height: 1.4; + white-space: pre; +} + +diagnostics-inspector .diagnostics-source-preview mark { + border-radius: 2px; + background: color-mix(in srgb, var(--ide-warning) 30%, transparent); + color: inherit; +} + +diagnostics-inspector .diagnostics-card-message { + color: var(--ide-text-muted); + font-size: 0.76rem; + line-height: 1.35; + white-space: pre-wrap; +} + +diagnostics-inspector .diagnostics-source-actions { + display: flex; + align-items: center; + gap: 6px; +} + +diagnostics-inspector .diagnostics-action { + min-width: 0; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-xs); + background: var(--ide-surface); + color: var(--ide-text-muted); + cursor: pointer; + font-size: 0.68rem; + line-height: 1; + padding: 6px 4px; +} + +diagnostics-inspector .diagnostics-action:hover:not(:disabled) { + background: var(--ide-surface-subtle); + border-color: var(--ide-border-strong); + color: var(--ide-text); +} + +diagnostics-inspector .diagnostics-action:disabled { + cursor: not-allowed; + opacity: 0.48; +} + +diagnostics-inspector .diagnostics-action i { + flex: 0 0 auto; + width: 0.82rem; + height: 0.82rem; +} + +diagnostics-inspector .diagnostics-action span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +diagnostics-inspector .diagnostics-footer { + border-top: 1px solid var(--ide-border); + background: var(--ide-surface-subtle); + color: var(--ide-text-muted); + font-size: 0.72rem; + line-height: 1.2; + padding: 6px 10px; +} + +@container (max-width: 220px) { + diagnostics-inspector .diagnostics-header { + gap: 6px; + min-height: auto; + padding: 8px; + } + + diagnostics-inspector .diagnostics-counts { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + diagnostics-inspector .diagnostics-count { + gap: 1px; + padding: 4px; + } + + diagnostics-inspector .diagnostics-count span { + font-size: 0.58rem; + } + + diagnostics-inspector .diagnostics-mode-tabs { + grid-template-columns: 1fr; + } + + diagnostics-inspector .diagnostics-mode-button { + justify-content: center; + padding: 5px 6px; + } + + diagnostics-inspector .diagnostics-mode-button span { + display: none; + } + + diagnostics-inspector .diagnostics-content { + padding: 6px; + } + + diagnostics-inspector .diagnostics-row { + border-radius: var(--ide-radius-xs); + } + + diagnostics-inspector .diagnostics-row-summary { + gap: 6px; + padding: 7px 6px; + } + + diagnostics-inspector .diagnostics-source-card { + padding: 7px; + } + + diagnostics-inspector .diagnostics-source-preview { + grid-template-columns: 28px minmax(0, 1fr); + } +} + +/* Scrollbar styling */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--ide-surface-subtle); +} + +::-webkit-scrollbar-thumb { + background: var(--ide-border-strong); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--ide-text-subtle); +} + +/* Toolbar */ +toolbar-panel { + display: grid; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + align-items: center; + padding: 8px 16px; + background: var(--ide-surface); + border-bottom: 1px solid var(--ide-border); + min-height: var(--ide-toolbar-height); + gap: 16px; + position: relative; + box-shadow: var(--ide-shadow-sm); +} + +toolbar-panel .title { + justify-self: start; + min-width: 0; + font-size: 1.125rem; + font-weight: 600; + color: var(--ide-text); + user-select: none; + letter-spacing: 0; +} + +toolbar-panel .status-container { + grid-column: 2; + justify-self: center; + display: flex; + align-items: center; + gap: 8px; + user-select: none; +} + +toolbar-panel .status-light { + width: 12px; + height: 12px; + border-radius: 50%; + box-shadow: 0 0 4px rgba(24, 26, 22, 0.2); + transition: all 0.3s ease; +} + +toolbar-panel .status-idle { + background: var(--ide-border-strong); + opacity: 0.5; +} + +toolbar-panel .status-running { + background: var(--ide-info); + animation: pulse 1.5s ease-in-out infinite; +} + +toolbar-panel .status-done { + background: var(--ide-success); +} + +toolbar-panel .status-error { + background: var(--ide-danger); +} + +toolbar-panel .status-aborted { + background: var(--ide-warning); +} + +toolbar-panel .status-fatal { + background: var(--ide-internal); +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + box-shadow: 0 0 4px color-mix(in srgb, var(--ide-info) 40%, transparent); + } + 50% { + opacity: 0.6; + box-shadow: 0 0 8px color-mix(in srgb, var(--ide-info) 70%, transparent); + } +} + +toolbar-panel .status-text { + font-size: 13px; + color: var(--ide-text-muted); + font-weight: 500; +} + +toolbar-panel .status-tooltip { + display: none; + font-size: 0.875rem; + color: var(--ide-text-muted); + position: absolute; + width: max-content; + top: 0; + left: 0; + background: var(--ide-surface); + border: 1px solid var(--ide-border); + padding: 0.25rem 0.5rem; + border-radius: var(--ide-radius-sm); + box-shadow: var(--ide-shadow-md); + z-index: var(--z-tooltip); +} + +toolbar-panel .actions { + justify-self: end; + display: flex; + align-items: center; + gap: 8px; +} + +toolbar-panel .compile-btn { + min-height: 32px; + height: 32px; + background: var(--ide-surface); + color: var(--ide-text-muted); + border: 1px solid var(--ide-border); + padding: 0 10px; + border-radius: var(--ide-radius-sm); + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 5px; + line-height: 1; +} + +toolbar-panel .compile-btn i { + width: 1rem; + height: 1rem; + margin-right: 0; +} + +toolbar-panel .toolbar-icon-btn { + width: 32px; + height: 32px; + flex: 0 0 32px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + background: var(--ide-surface); + color: var(--ide-text-muted); + cursor: pointer; + font-size: 0.95rem; + line-height: 1; + padding: 0; +} + +toolbar-panel .toolbar-icon-btn:hover { + background: var(--ide-accent-soft); + border-color: color-mix(in srgb, var(--ide-accent) 38%, var(--ide-border)); + color: var(--ide-accent-strong); +} + +toolbar-panel .toolbar-icon-btn i { + width: 1rem; + height: 1rem; +} + +toolbar-panel .compile-btn:hover { + background: var(--ide-accent-soft); + border-color: color-mix(in srgb, var(--ide-accent) 38%, var(--ide-border)); + color: var(--ide-accent-strong); +} + +toolbar-panel .compile-btn:active { + background: color-mix(in srgb, var(--ide-accent-soft) 82%, var(--ide-surface)); + transform: translateY(1px); +} + +toolbar-panel .compile-btn:disabled { + background: var(--ide-surface-muted); + color: var(--ide-text-subtle); + border-color: var(--ide-border); + cursor: not-allowed; + pointer-events: none; +} + +toolbar-panel .compile-btn.loading { + background: var(--ide-accent-soft); + border-color: var(--ide-accent); + color: var(--ide-accent-strong); + cursor: not-allowed; + pointer-events: none; +} + +toolbar-panel .compile-btn.loading:hover { + background: var(--ide-accent-soft); +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +toolbar-panel .compile-btn.loading i { + animation: spin 1s linear infinite; +} + +toolbar-panel .terminate-btn { + background: var(--ide-danger); + color: white; + border: 1px solid var(--ide-danger); + padding: 6px 16px; + border-radius: var(--ide-radius-sm); + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: background 0.2s ease; +} + +toolbar-panel .terminate-btn:hover { + background: color-mix(in srgb, var(--ide-danger) 88%, black); +} + +toolbar-panel .terminate-btn:active { + background: var(--ide-danger); + transform: translateY(1px); +} + +/* Bottom Panel */ +bottom-panel { + display: flex; + flex-direction: column; + background: var(--ide-surface); + border-top: 1px solid var(--ide-border); + position: relative; + min-height: var(--ide-bottom-panel-height); + max-height: var(--ide-bottom-panel-max-height); + transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1), + flex-basis 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +bottom-panel.collapsed { + min-height: var(--ide-bottom-panel-collapsed-height); + max-height: var(--ide-bottom-panel-collapsed-height); +} + +bottom-panel .bottom-panel-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 6px 10px; + background: var(--ide-surface-subtle); + border-bottom: 1px solid var(--ide-border); + min-height: var(--panel-header-height); +} + +bottom-panel .bottom-panel-tabs { + display: flex; + align-items: center; + gap: 4px; + min-width: 0; + flex: 0 0 auto; +} + +bottom-panel .bottom-panel-tab, +bottom-panel .clear-btn, +bottom-panel .download-btn, +bottom-panel .terminal-form button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 5px; + height: 28px; + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + background: var(--ide-surface); + color: var(--ide-text-muted); + cursor: pointer; + font-size: 0.8rem; + line-height: 1; + padding: 0 8px; +} + +bottom-panel .bottom-panel-tab:hover, +bottom-panel .bottom-panel-tab.active, +bottom-panel .download-btn:hover, +bottom-panel .terminal-form button:hover { + background: var(--ide-accent-soft); + border-color: color-mix(in srgb, var(--ide-accent) 38%, var(--ide-border)); + color: var(--ide-accent-strong); +} + +bottom-panel .bottom-panel-tab.active { + box-shadow: inset 0 -2px 0 var(--ide-accent); +} + +bottom-panel .bottom-panel-tab i, +bottom-panel .clear-btn i, +bottom-panel .download-btn i, +bottom-panel .terminal-form button i { + width: 0.9rem; + height: 0.9rem; +} + +bottom-panel .bottom-panel-tab span, +bottom-panel .clear-btn span, +bottom-panel .download-btn span, +bottom-panel .terminal-form button span { + line-height: 1; +} + +bottom-panel .bottom-panel-actions { + display: flex; + align-items: center; + gap: 6px; + min-width: 0; + justify-content: flex-end; + flex: 1 1 auto; +} + +bottom-panel .log-filter-controls { + display: inline-flex; + align-items: center; + gap: 6px; + min-width: 0; +} + +bottom-panel .log-filter-field { + display: inline-flex; + align-items: center; + gap: 4px; + min-width: 0; + color: var(--ide-text-muted); + font-size: 0.72rem; + white-space: nowrap; +} + +bottom-panel .log-filter-field select { + max-width: 128px; + min-width: 76px; + height: 28px; + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + background: var(--ide-surface); + color: var(--ide-text); + font: inherit; + padding: 3px 22px 3px 7px; +} + +bottom-panel .log-source-filter-field select { + max-width: 150px; +} + +bottom-panel .preserve-logs-label { + display: inline-flex; + align-items: center; + gap: 5px; + color: var(--ide-text-muted); + cursor: pointer; + font-size: 0.8rem; + font-weight: 450; + white-space: nowrap; +} + +bottom-panel .preserve-logs-checkbox { + --size: 0.875rem; + appearance: none; + -webkit-appearance: none; + width: var(--size); + height: var(--size); + border: 1.5px solid var(--ide-border-strong); + border-radius: var(--ide-radius-xs); + background: var(--ide-surface); + cursor: pointer; + position: relative; + flex: 0 0 auto; +} + +bottom-panel .preserve-logs-checkbox:checked { + background: var(--ide-accent); + border-color: var(--ide-accent); +} + +bottom-panel .preserve-logs-checkbox:checked::after { + content: ''; + position: absolute; + left: 50%; + top: 50%; + width: calc(var(--size) * 0.3125); + height: calc(var(--size) * 0.5625); + border: solid var(--ide-surface); + border-width: 0 2px 2px 0; + box-sizing: border-box; + transform: translate(-50%, -50%) translateY(-1px) rotate(45deg); +} + +bottom-panel .clear-btn { + background: var(--ide-danger-soft); + border-color: color-mix(in srgb, var(--ide-danger) 42%, var(--ide-border)); + color: var(--ide-danger); +} + +bottom-panel .clear-btn:hover { + background: color-mix(in srgb, var(--ide-danger-soft) 80%, var(--ide-danger)); + border-color: var(--ide-danger); + color: var(--ide-danger); +} + +bottom-panel.collapsed .bottom-panel-content { + display: none; +} + +bottom-panel.collapsed .clear-btn, +bottom-panel.collapsed .download-btn, +bottom-panel.collapsed .log-filter-controls, +bottom-panel.collapsed .preserve-logs-label { + display: none; +} + +bottom-panel .bottom-panel-content { + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + font-size: 0.875rem; +} + +bottom-panel .output-content, +bottom-panel .logging-content, +bottom-panel .terminal-content { + min-height: 100%; + padding: 6px 0; +} + +bottom-panel .console-message { + display: flex; + align-items: center; + gap: 8px; + min-height: 24px; + padding: 3px 12px; + border-bottom: 1px solid transparent; + font-family: var(--monospace); + line-height: 1.4; +} + +bottom-panel .console-message:hover { + background: var(--ide-surface-subtle); +} + +bottom-panel .console-icon { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + width: 16px; + line-height: 1; + text-align: center; +} + +bottom-panel .console-icon i { + display: inline-flex; + align-items: center; + justify-content: center; + line-height: 1; +} + +bottom-panel .console-text { + flex: 1; + min-width: 0; + line-height: 1.4; + word-break: break-word; + white-space: pre-wrap; +} + +bottom-panel .console-error { + color: var(--ide-danger); + background: color-mix(in srgb, var(--ide-danger) 7%, transparent); +} + +bottom-panel .console-warn { + color: var(--ide-warning); + background: color-mix(in srgb, var(--ide-warning) 7%, transparent); +} + +bottom-panel .console-info { + color: var(--ide-info); + background: color-mix(in srgb, var(--ide-info) 7%, transparent); +} + +bottom-panel .logging-content { + display: flex; + flex-direction: column; + gap: 0; + padding: 4px 0; +} + +bottom-panel .log-entry { + border-bottom: 1px solid var(--ide-border); + background: transparent; + min-width: 0; +} + +bottom-panel .log-entry:hover { + background: var(--ide-surface-subtle); +} + +bottom-panel .log-entry-row { + display: grid; + grid-template-columns: 16px 48px 72px minmax(54px, 92px) minmax(180px, 2fr) minmax(120px, 1.3fr); + align-items: center; + gap: 6px; + min-width: 0; + min-height: 25px; + padding: 3px 10px; + color: var(--ide-text-muted); + font-family: var(--monospace); + font-size: 0.72rem; + line-height: 1.25; +} + +bottom-panel summary.log-entry-row { + cursor: pointer; + list-style: none; +} + +bottom-panel summary.log-entry-row::-webkit-details-marker { + display: none; +} + +bottom-panel .log-entry-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 1rem; + height: 1rem; +} + +bottom-panel .log-entry-level { + border: 1px solid var(--ide-border); + border-radius: 999px; + background: var(--ide-surface-muted); + color: var(--ide-text-muted); + font-size: 0.58rem; + font-weight: 700; + letter-spacing: 0; + line-height: 1; + justify-self: start; + padding: 2px 5px; +} + +bottom-panel .log-level-error .log-entry-level, +bottom-panel .log-level-error .log-entry-icon { + color: var(--ide-danger); +} + +bottom-panel .log-level-warn .log-entry-level, +bottom-panel .log-level-warn .log-entry-icon { + color: var(--ide-warning); +} + +bottom-panel .log-level-info .log-entry-level, +bottom-panel .log-level-info .log-entry-icon { + color: var(--ide-info); +} + +bottom-panel .log-level-debug .log-entry-level, +bottom-panel .log-level-debug .log-entry-icon, +bottom-panel .log-level-trace .log-entry-level, +bottom-panel .log-level-trace .log-entry-icon { + color: var(--ide-text-muted); +} + +bottom-panel .log-entry-time, +bottom-panel .log-entry-source, +bottom-panel .log-entry-message, +bottom-panel .log-entry-body-preview { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +bottom-panel .log-entry-message { + color: var(--ide-text); + font-size: 0.75rem; +} + +bottom-panel .log-entry-body-preview { + color: var(--ide-text-subtle); +} + +bottom-panel .log-entry-body { + min-width: 0; +} + +bottom-panel .log-entry-body pre { + max-height: 180px; + margin: 1px 10px 6px 156px; + overflow: auto; + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-xs); + background: var(--ide-surface-subtle); + color: var(--ide-text); + font-family: var(--monospace); + font-size: 0.74rem; + line-height: 1.4; + padding: 7px; + white-space: pre-wrap; +} + +@media (max-width: 760px) { + bottom-panel .bottom-panel-header { + align-items: flex-start; + } + + bottom-panel .bottom-panel-actions { + flex-wrap: wrap; + } + + bottom-panel .log-filter-controls { + flex-wrap: wrap; + justify-content: flex-end; + } + + bottom-panel .log-filter-field span { + display: none; + } + + bottom-panel .log-entry-row { + grid-template-columns: 16px 44px 68px minmax(44px, 64px) minmax(110px, 1fr); + } + + bottom-panel .log-entry-body-preview { + display: none; + } + + bottom-panel .log-entry-body pre { + margin-left: 10px; + } +} + +bottom-panel .bottom-panel-empty { + display: flex; + align-items: center; + justify-content: center; + min-height: 110px; + color: var(--ide-text-muted); + font-size: 0.85rem; +} + +bottom-panel .bottom-panel-feedback { + margin: 6px 12px; + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + background: var(--ide-accent-soft); + color: var(--ide-accent-strong); + padding: 7px 9px; + font-size: 0.8rem; +} + +bottom-panel .terminal-content { + display: flex; + flex-direction: column; + gap: 6px; +} + +bottom-panel .terminal-transcript { + flex: 1; + margin: 0; + overflow: auto; + padding: 6px 12px; + color: var(--ide-text); + font-family: var(--monospace); + font-size: 0.84rem; + line-height: 1.45; + white-space: pre-wrap; +} + +bottom-panel .terminal-form { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 8px; + padding: 7px 12px; + border-top: 1px solid var(--ide-border); + color: var(--ide-text-muted); + font-family: var(--monospace); +} + +bottom-panel .terminal-input { + min-width: 0; + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + background: var(--ide-surface); + color: var(--ide-text); + font: inherit; + padding: 5px 7px; +} + +bottom-panel .terminal-input:focus { + outline: none; + border-color: var(--ide-accent); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--ide-accent) 22%, transparent); +} + +/* Native Dialogs */ +command-palette-dialog dialog, +share-dialog dialog, +settings-dialog dialog { + width: min(720px, calc(100vw - 32px)); + max-height: min(720px, calc(100vh - 48px)); + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + background: var(--ide-surface); + color: var(--ide-text); + box-shadow: var(--ide-shadow-md); + padding: 0; +} + +share-dialog dialog { + width: min(460px, calc(100vw - 32px)); +} + +command-palette-dialog dialog::backdrop, +share-dialog dialog::backdrop, +settings-dialog dialog::backdrop { + background: rgba(24, 26, 22, 0.36); +} + +command-palette-dialog dialog, +share-dialog dialog, +settings-dialog dialog { + position: fixed; + inset: 0; + margin: auto; + padding: 0; + border: 1px solid var(--ide-border-strong); + border-radius: var(--ide-radius-md); + background: var(--ide-surface); + color: var(--ide-text); + box-shadow: 0 24px 60px rgba(24, 26, 22, 0.28); + overflow: hidden; +} + +command-palette-dialog dialog { + --command-palette-top: clamp(48px, 16vh, 128px); + inset: var(--command-palette-top) 0 auto 0; + margin: 0 auto; + width: min(640px, calc(100vw - 32px)); + max-height: min(680px, calc(100vh - var(--command-palette-top) - 24px)); +} + +share-dialog dialog { + width: min(360px, calc(100vw - 32px)); + max-height: min(260px, calc(100vh - 32px)); +} + +settings-dialog dialog { + width: min(760px, calc(100vw - 32px)); + height: min(560px, calc(100vh - 48px)); + max-height: min(560px, calc(100vh - 48px)); +} + +settings-dialog dialog.settings-dialog-native, +project-switcher dialog.project-switcher-native { + opacity: 1; + transform: translateY(0) scale(1); + transition: + opacity 150ms ease, + transform 170ms cubic-bezier(0.16, 1, 0.3, 1), + overlay 170ms allow-discrete, + display 170ms allow-discrete; + transition-behavior: allow-discrete; + will-change: opacity, transform; +} + +settings-dialog dialog.settings-dialog-native::backdrop, +project-switcher dialog.project-switcher-native::backdrop { + transition: + background 170ms ease, + overlay 170ms allow-discrete, + display 170ms allow-discrete; + transition-behavior: allow-discrete; +} + +@starting-style { + settings-dialog dialog.settings-dialog-native[open], + project-switcher dialog.project-switcher-native[open] { + opacity: 0; + transform: translateY(8px) scale(0.985); + } + + settings-dialog dialog.settings-dialog-native[open]::backdrop, + project-switcher dialog.project-switcher-native[open]::backdrop { + background: rgba(24, 26, 22, 0); + } +} + +settings-dialog dialog.settings-dialog-native[data-dialog-closing="true"], +project-switcher dialog.project-switcher-native[data-dialog-closing="true"] { + opacity: 0; + transform: translateY(6px) scale(0.985); +} + +settings-dialog dialog.settings-dialog-native[data-dialog-closing="true"]::backdrop, +project-switcher dialog.project-switcher-native[data-dialog-closing="true"]::backdrop { + background: rgba(24, 26, 22, 0); +} + +@media (prefers-reduced-motion: reduce) { + settings-dialog dialog.settings-dialog-native, + project-switcher dialog.project-switcher-native, + settings-dialog dialog.settings-dialog-native::backdrop, + project-switcher dialog.project-switcher-native::backdrop { + transition: none; + } +} + +.command-palette-form, +.share-dialog-form, +.settings-dialog-form { + display: flex; + flex-direction: column; + min-height: 0; +} + +.settings-dialog-form { + height: 100%; +} + +.command-palette-form { + max-height: inherit; +} + +.command-search-input { + margin: 10px 10px 6px; + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + background: var(--ide-surface); + color: var(--ide-text); + font: inherit; + font-size: 0.86rem; + padding: 7px 9px; +} + +.command-search-input:focus { + outline: none; + border-color: var(--ide-accent); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--ide-accent) 22%, transparent); +} + +.command-list { + flex: 1 1 auto; + display: flex; + flex-direction: column; + gap: 4px; + min-height: 0; + overflow-y: auto; + padding: 0 8px 8px; +} + +.command-row { + width: 100%; + display: grid; + grid-template-columns: auto minmax(0, 1fr); + align-items: center; + gap: 8px; + border: 1px solid transparent; + border-radius: var(--ide-radius-sm); + background: transparent; + color: var(--ide-text); + cursor: pointer; + padding: 5px 7px; + text-align: left; +} + +.command-row:hover:not(:disabled), +.command-row.active:not(:disabled) { + background: var(--ide-surface-subtle); + border-color: var(--ide-border); +} + +.command-row.disabled { + cursor: not-allowed; + opacity: 0.52; +} + +.command-row > i { + width: 0.86rem; + height: 0.86rem; + color: var(--ide-text-muted); +} + +.command-symbol-row { + grid-template-columns: 30px minmax(0, 1fr); +} + +.command-symbol-kind { + display: inline-flex; + align-items: center; + justify-content: center; + width: 30px; + min-height: 18px; + border: 1px solid color-mix(in srgb, var(--outline-symbol) 38%, var(--ide-border)); + border-radius: var(--ide-radius-sm); + background: color-mix(in srgb, var(--outline-symbol) 12%, var(--ide-surface)); + color: var(--outline-symbol-strong); + font-size: 0.68rem; + font-weight: 700; + line-height: 1; + padding: 0 4px; + text-align: center; +} + +.command-row-main { + display: grid; + grid-template-columns: minmax(0, auto) minmax(0, 1fr); + align-items: baseline; + gap: 8px; + min-width: 0; +} + +.command-symbol-main { + grid-template-columns: minmax(0, 1fr); + gap: 2px; +} + +.command-row-title { + overflow: hidden; + color: var(--ide-text); + font-size: 0.82rem; + font-weight: 600; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.command-row-detail { + overflow: hidden; + color: var(--ide-text-muted); + font-size: 0.74rem; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.command-symbol-origin { + margin-left: 6px; + color: var(--ide-text-subtle); + font-size: 0.7rem; + text-transform: uppercase; +} + +.command-result-count { + color: var(--ide-text-muted); + font-size: 0.72rem; + padding: 3px 7px 5px; +} + +.command-index-status { + color: var(--ide-text-muted); + font-size: 0.72rem; + padding: 3px 7px 5px; +} + +.dialog-empty { + color: var(--ide-text-muted); + padding: 16px 8px; + text-align: center; +} + +.share-dialog-titlebar { + padding: 12px 14px; + border-bottom: 1px solid var(--ide-border); + background: var(--ide-surface-subtle); +} + +.share-dialog-titlebar h2 { + color: var(--ide-text); + font-size: 0.95rem; + font-weight: 700; + line-height: 1.2; + margin: 0; +} + +.share-actions { + display: flex; + flex-direction: column; + justify-content: center; + gap: 8px; + padding: 14px; +} + +.settings-dialog-titlebar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 12px 14px; + border-bottom: 1px solid var(--ide-border); + background: var(--ide-surface-subtle); +} + +.settings-dialog-titlebar h2 { + margin: 0; + color: var(--ide-text); + font-size: 0.95rem; + font-weight: 700; + line-height: 1.2; +} + +.settings-dialog-close { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border: 1px solid transparent; + border-radius: var(--ide-radius-sm); + background: transparent; + color: var(--ide-text-muted); + cursor: pointer; + padding: 0; +} + +.settings-dialog-close:hover { + background: var(--ide-surface-muted); + color: var(--ide-text); +} + +.settings-dialog-body { + display: grid; + grid-template-columns: 180px minmax(0, 1fr); + flex: 1; + min-height: 0; +} + +.settings-tabs { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; + padding: 10px; + background: var(--ide-surface-subtle); + border-right: 1px solid var(--ide-border); +} + +.settings-tab { + border: 1px solid transparent; + border-radius: var(--ide-radius-sm); + background: transparent; + color: var(--ide-text-muted); + cursor: pointer; + font: inherit; + font-size: 0.82rem; + font-variation-settings: 'wght' 520; + line-height: 1.2; + padding: 7px 9px; + text-align: left; +} + +.settings-tab:hover, +.settings-tab.active { + background: var(--ide-accent-soft); + border-color: color-mix(in srgb, var(--ide-accent) 34%, var(--ide-border)); + color: var(--ide-accent-strong); + font-variation-settings: 'wght' 580; +} + +.settings-tab-panels { + min-height: 0; + overflow: auto; + padding: 12px 14px; +} + +.settings-tab-content { + display: none; + gap: 8px; +} + +.settings-tab-content.active { + display: grid; +} + +.settings-cascade { + display: grid; + gap: 8px; +} + +.settings-cascade-children { + display: none; + gap: 8px; + margin-left: 12px; + padding-left: 12px; + border-left: 1px solid color-mix(in srgb, var(--ide-border-strong) 72%, transparent); +} + +.settings-cascade.settings-cascade-open .settings-cascade-children { + display: grid; +} + +.settings-cascade-children .settings-row { + background: color-mix(in srgb, var(--ide-surface) 74%, var(--ide-surface-subtle)); +} + +.settings-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 12px; + min-height: 34px; + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + background: var(--ide-surface); + color: var(--ide-text); + font-size: 0.82rem; + padding: 7px 9px; +} + +.settings-row-label { + min-width: 0; + color: var(--ide-text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.settings-switch-control { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 36px; + height: 20px; +} + +.settings-switch-input { + position: absolute; + inset: 0; + width: 36px; + height: 20px; + margin: 0; + opacity: 0; + cursor: pointer; +} + +.settings-switch-track { + display: inline-flex; + align-items: center; + width: 36px; + height: 20px; + border: 1px solid var(--ide-border-strong); + border-radius: 999px; + background: var(--ide-surface-muted); + transition: background 0.15s ease, border-color 0.15s ease; +} + +.settings-switch-thumb { + width: 16px; + height: 16px; + margin-left: 1px; + border-radius: 50%; + background: var(--ide-surface); + box-shadow: 0 1px 2px rgba(24, 26, 22, 0.18); + transition: transform 0.15s ease; +} + +.settings-switch-input:checked + .settings-switch-track { + border-color: var(--ide-accent); + background: var(--ide-accent); +} + +.settings-switch-input:checked + .settings-switch-track .settings-switch-thumb { + transform: translateX(16px); +} + +.settings-switch-input:focus-visible + .settings-switch-track { + outline: 2px solid color-mix(in srgb, var(--ide-accent) 42%, transparent); + outline-offset: 2px; +} + +.settings-input { + min-width: 160px; + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + background: var(--ide-surface); + color: var(--ide-text); + font: inherit; + font-size: 0.82rem; + padding: 5px 7px; +} + +.settings-select-root { + position: relative; + min-width: 180px; +} + +.settings-select-trigger { + display: inline-flex; + align-items: center; + justify-content: space-between; + gap: 8px; + width: 100%; + min-height: 30px; + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + background: var(--ide-surface); + color: var(--ide-text); + cursor: pointer; + font: inherit; + font-size: 0.82rem; + padding: 5px 7px 5px 9px; +} + +.settings-select-trigger:hover, +.settings-select-trigger[aria-expanded="true"] { + border-color: color-mix(in srgb, var(--ide-accent) 42%, var(--ide-border)); + background: var(--ide-surface-subtle); +} + +.settings-select-trigger:focus-visible { + outline: 2px solid color-mix(in srgb, var(--ide-accent) 36%, transparent); + outline-offset: 2px; +} + +.settings-select-trigger > i { + color: var(--ide-text-subtle); + font-size: 0.78rem; +} + +.settings-select-content { + position: absolute; + z-index: 40; + top: calc(100% + 4px); + right: 0; + width: 100%; + max-height: 220px; + overflow: auto; + border: 1px solid var(--ide-border-strong); + border-radius: var(--ide-radius-sm); + background: var(--ide-surface); + box-shadow: var(--ide-shadow-md); + padding: 4px; +} + +.settings-select-option { + display: flex; + align-items: center; + width: 100%; + min-height: 28px; + border: 1px solid transparent; + border-radius: var(--ide-radius-sm); + background: transparent; + color: var(--ide-text); + cursor: pointer; + font: inherit; + font-size: 0.82rem; + padding: 5px 7px; + text-align: left; +} + +.settings-select-option:hover, +.settings-select-option:focus-visible, +.settings-select-option[aria-selected="true"] { + background: var(--ide-accent-soft); + border-color: color-mix(in srgb, var(--ide-accent) 30%, var(--ide-border)); + color: var(--ide-accent-strong); + outline: none; +} + +.settings-input { + min-width: min(340px, 46vw); +} + +.settings-number-input { + min-width: 72px; + width: 72px; +} + +.settings-action-button { + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + background: var(--ide-surface); + color: var(--ide-text); + cursor: pointer; + font: inherit; + font-size: 0.82rem; + padding: 5px 9px; +} + +.settings-action-button:hover { + background: var(--ide-accent-soft); + border-color: color-mix(in srgb, var(--ide-accent) 34%, var(--ide-border)); + color: var(--ide-accent-strong); +} + +.settings-row-static .settings-row-value { + color: var(--ide-text-muted); + font-size: 0.78rem; +} + +.settings-row-value a { + color: var(--ide-accent-strong); + text-decoration: none; + text-underline-offset: 2px; +} + +.settings-row-value a:hover { + color: var(--ide-accent); + text-decoration: underline; +} + +.settings-data-overview { + display: grid; + gap: 8px; +} + +.share-download-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + width: 100%; + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + background: var(--ide-text); + color: var(--ide-surface); + cursor: pointer; + font-size: 0.9rem; + padding: 7px 10px; +} + +.share-download-button:hover { + background: color-mix(in srgb, var(--ide-text) 88%, var(--ide-accent)); +} + +/* Resize Handles */ +resize-handle { + position: absolute; + z-index: 100; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + transition: background-color 0.2s ease; +} + +resize-handle.resize-handle-horizontal { + top: 0; + width: 10px; + height: 100%; + cursor: ew-resize; +} + +resize-handle.resize-handle-horizontal[side="left"] { + right: 0; + transform: translateX(50%); +} + +resize-handle.resize-handle-horizontal[side="right"] { + left: 0; + transform: translateX(-50%); +} + +resize-handle.resize-handle-vertical { + left: 0; + width: 100%; + height: 10px; + cursor: ns-resize; +} + +resize-handle.resize-handle-vertical[side="top"] { + top: 0; + transform: translateY(-50%); +} + +resize-handle.resize-handle-vertical[side="bottom"] { + bottom: 0; + transform: translateY(50%); +} + +resize-handle:hover { + background: color-mix(in srgb, var(--ide-accent) 55%, transparent); +} + +resize-handle.active { + background: var(--ide-accent); +} + +resize-handle .resize-handle-indicator { + pointer-events: none; + opacity: 0; + transition: opacity 0.2s ease; +} + +resize-handle.resize-handle-horizontal .resize-handle-indicator { + width: 2px; + height: 40px; + background: var(--ide-accent-strong); + border-radius: 2px; +} + +resize-handle.resize-handle-vertical .resize-handle-indicator { + width: 40px; + height: 2px; + background: var(--ide-accent-strong); + border-radius: 2px; +} + +resize-handle:hover .resize-handle-indicator, +resize-handle.active .resize-handle-indicator { + opacity: 1; +} + +/* Hide resize handles when panels are collapsed */ +file-explorer.collapsed resize-handle, +diagnostics-inspector.collapsed resize-handle, +bottom-panel.collapsed resize-handle { + display: none; +} + +/* ───── Project switcher pill (toolbar) ───── */ + +toolbar-panel .toolbar-leading { + justify-self: start; + display: inline-flex; + align-items: center; + gap: 12px; + min-width: 0; + max-width: 100%; +} + +toolbar-panel .toolbar-brand { + display: inline-flex; + align-items: baseline; + user-select: none; + font-size: 1.18rem; + line-height: 1; + color: var(--ide-text); + letter-spacing: -0.005em; +} + +toolbar-panel .toolbar-brand-sans { + font-family: var(--sans-serif); + font-weight: 600; +} + +toolbar-panel .toolbar-brand-italic { + font-family: 'Instrument Serif', 'Iowan Old Style', 'Apple Garamond', 'Baskerville', Georgia, serif; + font-style: italic; + font-weight: 400; + color: var(--ide-accent); + font-size: 1.28rem; + line-height: 1; + margin-left: 1px; +} + +toolbar-panel .project-switcher-pill { + display: inline-flex; + align-items: center; + gap: 8px; + max-width: 100%; + padding: 4px 10px 4px 6px; + background: var(--ide-surface-subtle); + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + color: var(--ide-text); + cursor: pointer; + font-family: inherit; + font-size: 0.9rem; + font-weight: 600; + line-height: 1.1; + transition: background 0.12s ease, border-color 0.12s ease; +} + +toolbar-panel .project-switcher-pill:hover { + background: var(--ide-surface); + border-color: var(--ide-border-strong); +} + +toolbar-panel .project-switcher-pill:focus-visible { + outline: none; + border-color: var(--ide-accent); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--ide-accent) 22%, transparent); +} + +toolbar-panel .project-switcher-pill-glyph { + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + border-radius: var(--ide-radius-sm); + background: var(--ide-accent-soft); + color: var(--ide-accent-strong); + font-size: 0.78rem; + font-weight: 700; +} + +toolbar-panel .project-switcher-pill-name { + overflow: hidden; + max-width: 220px; + text-overflow: ellipsis; + white-space: nowrap; +} + +toolbar-panel .project-switcher-pill-caret { + color: var(--ide-text-subtle); + font-size: 0.7rem; +} + +/* ───── Project switcher modal ───── */ + +project-switcher dialog.project-switcher-native { + position: fixed; + inset: 0; + margin: auto; + padding: 0; + width: min(1100px, calc(100vw - 48px)); + height: min(720px, calc(100vh - 48px)); + border: 1px solid var(--ide-border-strong); + border-radius: var(--ide-radius-md); + background: var(--ide-surface); + color: var(--ide-text); + box-shadow: 0 24px 60px rgba(24, 26, 22, 0.28); + overflow: hidden; +} + +project-switcher dialog.project-switcher-native::backdrop { + background: rgba(24, 26, 22, 0.36); +} + +.project-switcher-form { + display: grid; + grid-template-rows: auto 1fr auto; + height: 100%; + min-height: 0; +} + +.project-switcher-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + padding: 14px 18px 10px; + border-bottom: 1px solid var(--ide-border); +} + +.project-switcher-titles h2 { + margin: 0; + font-size: 1rem; + font-weight: 600; +} + +.project-switcher-titles p { + margin: 2px 0 0; + font-size: 0.78rem; + color: var(--ide-text-muted); +} + +.project-switcher-close { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border: 1px solid transparent; + border-radius: var(--ide-radius-sm); + background: transparent; + color: var(--ide-text-muted); + padding: 0; + line-height: 0; + cursor: pointer; +} + +.project-switcher-close > i { + display: inline-block; + line-height: 1; +} + +.project-switcher-close:hover { + background: var(--ide-surface-subtle); + color: var(--ide-text); +} + +.project-switcher-body { + display: grid; + grid-template-columns: max-content minmax(0, 1fr); + min-height: 0; + overflow: hidden; +} + +.project-switcher-list { + width: 280px; + min-width: 200px; + max-width: min(440px, 45vw); + border-right: 1px solid var(--ide-border); + background: var(--ide-surface-subtle); + overflow: auto; + padding: 8px; + resize: horizontal; +} + +.project-switcher-card { + display: grid; + grid-template-columns: 28px minmax(0, 1fr); + align-items: center; + gap: 10px; + width: 100%; + padding: 8px 10px; + border: 1px solid transparent; + border-radius: var(--ide-radius-sm); + background: transparent; + color: var(--ide-text); + cursor: pointer; + text-align: left; + user-select: none; +} + +.project-switcher-card + .project-switcher-card { + margin-top: 4px; +} + +.project-switcher-card:hover { + background: var(--ide-surface); + border-color: var(--ide-border); +} + +.project-switcher-card.sel { + background: var(--ide-accent-soft); + border-color: color-mix(in srgb, var(--ide-accent) 38%, var(--ide-border)); +} + +.project-switcher-card.current .project-switcher-glyph { + outline: 2px solid color-mix(in srgb, var(--ide-accent) 60%, transparent); + outline-offset: 1px; +} + +.project-switcher-glyph { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: var(--ide-radius-sm); + background: var(--ide-accent-soft); + color: var(--ide-accent-strong); + font-weight: 700; + font-size: 0.86rem; +} + +.project-switcher-glyph.large { + width: 36px; + height: 36px; + font-size: 1rem; +} + +.project-switcher-card-body { + display: grid; + gap: 2px; + min-width: 0; +} + +.project-switcher-card-name { + display: flex; + align-items: center; + gap: 6px; + font-weight: 600; + font-size: 0.86rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.project-switcher-current-pill { + font-size: 0.66rem; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--ide-accent-strong); + background: var(--ide-surface); + border: 1px solid color-mix(in srgb, var(--ide-accent) 40%, var(--ide-border)); + border-radius: 999px; + padding: 1px 7px; +} + +.project-switcher-card-meta { + font-size: 0.74rem; + color: var(--ide-text-muted); +} + +.project-switcher-detail { + display: flex; + flex-direction: column; + min-width: 0; + min-height: 0; + padding: 14px 18px 16px; + gap: 10px; + overflow: hidden; +} + +.project-switcher-empty { + display: grid; + place-items: center; + height: 100%; + color: var(--ide-text-muted); + font-size: 0.86rem; +} + +.project-switcher-detail-head { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + align-items: flex-start; + gap: 12px; +} + +.project-switcher-detail-titles h3 { + margin: 0; + font-size: 1rem; + font-weight: 600; +} + +.project-switcher-detail-titles p { + margin: 4px 0 0; + font-size: 0.78rem; + color: var(--ide-text-muted); +} + +.project-switcher-detail-actions { + display: flex; + flex-wrap: wrap; + gap: 6px; + align-items: center; + justify-content: flex-end; +} + +.project-switcher-detail-actions button { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 5px 10px; + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + background: var(--ide-surface); + color: var(--ide-text); + font: inherit; + font-size: 0.78rem; + cursor: pointer; +} + +.project-switcher-detail-actions button:hover:not([disabled]) { + background: var(--ide-surface-subtle); +} + +.project-switcher-detail-actions button[disabled] { + opacity: 0.5; + cursor: not-allowed; +} + +.project-switcher-detail-actions .project-switcher-open { + background: var(--ide-accent); + border-color: var(--ide-accent-strong); + color: var(--ide-surface); +} + +.project-switcher-detail-actions .project-switcher-open:hover:not([disabled]) { + background: var(--ide-accent-strong); +} + +.project-switcher-detail-actions .project-switcher-open.is-current { + background: var(--ide-surface); + color: var(--ide-text-muted); + border-color: var(--ide-border); +} + +.project-switcher-detail-actions .project-switcher-delete { + background: var(--ide-danger-soft); + border-color: color-mix(in srgb, var(--ide-danger) 50%, var(--ide-border)); + color: var(--ide-danger); +} + +.project-switcher-detail-actions .project-switcher-delete:hover:not([disabled]) { + background: color-mix(in srgb, var(--ide-danger) 16%, var(--ide-surface)); + border-color: var(--ide-danger); +} + +.project-switcher-stats { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 4px 18px; + margin: 0; + padding: 8px 12px; + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + background: var(--ide-surface-subtle); + align-items: baseline; +} + +.project-switcher-stats > div { + display: inline-flex; + align-items: baseline; + gap: 6px; + min-width: 0; +} + +.project-switcher-stats dt { + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--ide-text-subtle); + flex: 0 0 auto; +} + +.project-switcher-stats dd { + margin: 0; + font-size: 0.82rem; + color: var(--ide-text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; +} + +.project-switcher-stats .mono dd { + font-family: var(--monospace); + font-size: 0.76rem; +} + +.project-switcher-detail-note { + margin-top: 14px; + display: flex; + align-items: center; + gap: 8px; + font-size: 0.78rem; + color: var(--ide-text-muted); +} + +.project-switcher-preview { + margin: 0; + display: grid; + grid-template-columns: max-content minmax(0, 1fr); + gap: 0; + flex: 1 1 auto; + min-height: 220px; + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + overflow: hidden; + background: var(--ide-surface); +} + +.project-switcher-preview-tree { + width: 220px; + min-width: 160px; + max-width: min(420px, 55vw); + display: flex; + flex-direction: column; + background: var(--ide-surface-subtle); + border-right: 1px solid var(--ide-border); + padding: 4px; + overflow: auto; + min-height: 0; + resize: horizontal; +} + +.ps-preview-row { + display: flex; + align-items: center; + gap: 4px; + text-align: left; + background: transparent; + border: 1px solid transparent; + border-radius: var(--ide-radius-sm); + color: var(--ide-text); + font: inherit; + font-size: 0.78rem; + padding: 3px 6px; + user-select: none; +} + +.ps-preview-file, +.ps-preview-folder { + cursor: pointer; +} + +.ps-preview-file:hover, +.ps-preview-folder:hover { + background: var(--ide-surface); + border-color: var(--ide-border); +} + +button.ps-preview-folder { + width: 100%; +} + +.ps-preview-row.active { + background: var(--ide-accent-soft); + border-color: color-mix(in srgb, var(--ide-accent) 38%, var(--ide-border)); + color: var(--ide-accent-strong); +} + +.ps-preview-row > i { + color: var(--ide-text-subtle); + font-size: 0.82rem; + flex: 0 0 auto; +} + +.ps-preview-folder { + color: var(--ide-text-muted); + font-weight: 600; +} + +.ps-preview-folder > i.icon-folder-open { + color: var(--ide-accent); +} + +.ps-preview-folder-chevron { + font-size: 0.7rem !important; +} + +.ps-preview-row > span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1 1 auto; +} + +.project-switcher-preview-empty { + padding: 14px 8px; + font-size: 0.78rem; + color: var(--ide-text-muted); + text-align: center; +} + +.project-switcher-preview-content { + display: flex; + flex-direction: column; + min-width: 0; + min-height: 0; +} + +.project-switcher-preview-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 4px 10px; + border-bottom: 1px solid var(--ide-border); + background: var(--ide-surface-subtle); +} + +.project-switcher-preview-path { + font-family: inherit; + font-size: 0.74rem; + color: var(--ide-text-muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + flex: 1 1 auto; +} + +.project-switcher-preview-mtime { + font-family: inherit; + font-size: 0.7rem; + color: var(--ide-text-subtle); + white-space: nowrap; + flex: 0 0 auto; +} + +.project-switcher-preview-body { + flex: 1 1 auto; + min-height: 0; + overflow: hidden; + background: var(--ide-surface); + position: relative; +} + +.project-switcher-preview-body .cm-editor { + height: 100%; +} + +.project-switcher-preview-body .cm-scroller { + font-family: var(--monospace); + font-size: 0.78rem; +} + +.project-switcher-foot { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 18px; + border-top: 1px solid var(--ide-border); + background: var(--ide-surface-subtle); +} + +.project-switcher-new, +.project-switcher-import { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 5px 11px; + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + background: var(--ide-surface); + color: var(--ide-text); + font: inherit; + font-size: 0.82rem; + cursor: pointer; +} + +.project-switcher-new:hover, +.project-switcher-import:hover { + background: var(--ide-surface-subtle); +} + +.project-switcher-import-error { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 3px 8px; + border-radius: var(--ide-radius-sm); + background: var(--ide-danger-soft); + color: var(--ide-danger); + font-size: 0.74rem; +} + +/* Sub-dialogs (create / delete) */ + +project-switcher dialog.project-switcher-sub { + position: fixed; + inset: 0; + margin: auto; + padding: 0; + width: min(420px, calc(100vw - 32px)); + border: 1px solid var(--ide-border-strong); + border-radius: var(--ide-radius-md); + background: var(--ide-surface); + color: var(--ide-text); + box-shadow: 0 18px 50px rgba(24, 26, 22, 0.30); + overflow: hidden; +} + +project-switcher dialog.project-switcher-sub-wide { + width: min(540px, calc(100vw - 32px)); +} + +.project-switcher-import-hints { + margin: 0; + padding-left: 18px; + display: grid; + gap: 4px; + color: var(--ide-text-muted); + font-size: 0.78rem; +} + +.project-switcher-import-hints code { + font-family: var(--monospace); + font-size: 0.74rem; + background: var(--ide-surface-subtle); + padding: 0 4px; + border-radius: 3px; +} + +.project-switcher-import-error { + margin: 0; + padding: 6px 10px; + border: 1px solid color-mix(in srgb, var(--ide-danger) 30%, var(--ide-border)); + border-radius: var(--ide-radius-sm); + background: var(--ide-danger-soft); + color: var(--ide-danger); + font-size: 0.78rem; +} + +.project-switcher-sub-actions .project-switcher-choose-file { + display: inline-flex; + align-items: center; + gap: 6px; + background: var(--ide-accent); + border-color: var(--ide-accent-strong); + color: var(--ide-surface); +} + +.project-switcher-sub-actions .project-switcher-choose-file:hover { + background: var(--ide-accent-strong); +} + +project-switcher dialog.project-switcher-sub::backdrop { + background: rgba(24, 26, 22, 0.44); +} + +.project-switcher-sub-form { + padding: 16px 18px; + display: grid; + gap: 12px; +} + +.project-switcher-sub-form h3 { + margin: 0; + font-size: 0.96rem; + font-weight: 600; +} + +.project-switcher-hint { + margin: 0; + font-size: 0.78rem; + color: var(--ide-text-muted); +} + +.project-switcher-field { + display: grid; + gap: 4px; + font-size: 0.78rem; + color: var(--ide-text-muted); +} + +.project-switcher-field input { + font: inherit; + font-size: 0.86rem; + padding: 6px 9px; + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + background: var(--ide-surface); + color: var(--ide-text); +} + +.project-switcher-field input:focus { + outline: none; + border-color: var(--ide-accent); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--ide-accent) 22%, transparent); +} + +.project-switcher-delete-target { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + align-items: center; + gap: 10px; + padding: 10px 12px; + background: var(--ide-surface-subtle); + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); +} + +.project-switcher-delete-target .n { + font-weight: 600; +} + +.project-switcher-delete-target .m { + font-size: 0.74rem; + color: var(--ide-text-muted); +} + +.project-switcher-sub-actions { + display: flex; + justify-content: flex-end; + gap: 6px; +} + +.project-switcher-sub-actions button { + padding: 6px 12px; + border: 1px solid var(--ide-border); + border-radius: var(--ide-radius-sm); + background: var(--ide-surface); + color: var(--ide-text); + font: inherit; + font-size: 0.82rem; + cursor: pointer; +} + +.project-switcher-sub-actions .project-switcher-confirm-create, +.project-switcher-sub-actions .project-switcher-confirm-rename { + background: var(--ide-accent); + border-color: var(--ide-accent-strong); + color: var(--ide-surface); +} + +.project-switcher-sub-actions .project-switcher-confirm-create:hover, +.project-switcher-sub-actions .project-switcher-confirm-rename:hover { + background: var(--ide-accent-strong); +} + +.project-switcher-sub-actions .project-switcher-confirm-delete { + background: var(--ide-danger); + border-color: color-mix(in srgb, var(--ide-danger) 70%, black); + color: var(--ide-surface); +} + +.project-switcher-sub-actions .project-switcher-confirm-delete:hover { + background: color-mix(in srgb, var(--ide-danger) 88%, black); +} diff --git a/hkmc2/shared/src/test/mlscript/HkScratch.mls b/hkmc2/shared/src/test/mlscript/HkScratch.mls index c049eb68f7..03dd943a38 100644 --- a/hkmc2/shared/src/test/mlscript/HkScratch.mls +++ b/hkmc2/shared/src/test/mlscript/HkScratch.mls @@ -27,3 +27,21 @@ set y = 1 //β”‚ ═══[RUNTIME ERROR] TypeError: 0 is not a function +:sjs +fun errorMessage(id, error) = + mut + 'type: "compile-error" + id: id + name: error.name + message: error.message + stack: error.stack +//β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” +//β”‚ let errorMessage; +//β”‚ errorMessage = function errorMessage(id, error) { +//β”‚ let name, message, stack; +//β”‚ name = error.name; +//β”‚ message = error.message; +//β”‚ stack = error.stack; +//β”‚ return { type: "compile-error", id: id, name: name, message: message, stack: stack } +//β”‚ }; +//β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” diff --git a/hkmc2/shared/src/test/mlscript/apps/parsing/PrattParsingTest.mls b/hkmc2/shared/src/test/mlscript/apps/parsing/PrattParsingTest.mls index 4aca5615ea..701e6d691e 100644 --- a/hkmc2/shared/src/test/mlscript/apps/parsing/PrattParsingTest.mls +++ b/hkmc2/shared/src/test/mlscript/apps/parsing/PrattParsingTest.mls @@ -1,9 +1,9 @@ :js -import "../../../mlscript-compile/apps/parsing/PrattParsing.mls" -import "../../../mlscript-compile/apps/parsing/Expr.mls" -import "../../../mlscript-compile/apps/parsing/TokenHelpers.mls" -import "../../../mlscript-compile/apps/parsing/Lexer.mls" +import "std/apps/parsing/PrattParsing.mls" +import "std/apps/parsing/Expr.mls" +import "std/apps/parsing/TokenHelpers.mls" +import "std/apps/parsing/Lexer.mls" open Lexer { lex } open PrattParsing { parse } diff --git a/hkmc2/shared/src/test/mlscript/backlog/NonReturningStatements.mls b/hkmc2/shared/src/test/mlscript/backlog/NonReturningStatements.mls index 3557776d76..12c4e986c9 100644 --- a/hkmc2/shared/src/test/mlscript/backlog/NonReturningStatements.mls +++ b/hkmc2/shared/src/test/mlscript/backlog/NonReturningStatements.mls @@ -31,7 +31,7 @@ fun foo = :sjs foo //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ Predef.print(1); Predef.print("..."); return runtime.Unit +//β”‚ Predef.print(1); Predef.print("..."); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ > 1 //β”‚ > ... diff --git a/hkmc2/shared/src/test/mlscript/backlog/ToTriage.mls b/hkmc2/shared/src/test/mlscript/backlog/ToTriage.mls index fe15640596..d2a2d74ccb 100644 --- a/hkmc2/shared/src/test/mlscript/backlog/ToTriage.mls +++ b/hkmc2/shared/src/test/mlscript/backlog/ToTriage.mls @@ -48,7 +48,7 @@ val Infinity = 1 :sjs Infinity //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return Infinity +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = Infinity @@ -219,7 +219,7 @@ open Stack { Nil, :: } :sjs 1 :: Nil //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return Stack.Cons(1, Stack.Nil) +//β”‚ Stack.Cons(1, Stack.Nil); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = Cons(1, Nil) diff --git a/hkmc2/shared/src/test/mlscript/basics/AppOp.mls b/hkmc2/shared/src/test/mlscript/basics/AppOp.mls index a4b8ddb8b7..195b56c528 100644 --- a/hkmc2/shared/src/test/mlscript/basics/AppOp.mls +++ b/hkmc2/shared/src/test/mlscript/basics/AppOp.mls @@ -43,7 +43,7 @@ add@1@2 :fixme @(id, 123) //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return Predef.apply(123) +//β”‚ Predef.apply(123); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ ═══[RUNTIME ERROR] TypeError: f is not a function diff --git a/hkmc2/shared/src/test/mlscript/basics/Assert.mls b/hkmc2/shared/src/test/mlscript/basics/Assert.mls index 55fc7befd5..fb4c3963fc 100644 --- a/hkmc2/shared/src/test/mlscript/basics/Assert.mls +++ b/hkmc2/shared/src/test/mlscript/basics/Assert.mls @@ -7,15 +7,12 @@ let xs = [1, 2, 3] :sjs assert true //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ if (true === true) { -//β”‚ return runtime.Unit -//β”‚ } -//β”‚ return runtime.safeCall(runtime.assertFail("Assert.mls", "9")); +//β”‚ if (true !== true) { runtime.safeCall(runtime.assertFail("Assert.mls", "9")); } //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” :re assert false -//β”‚ ═══[RUNTIME ERROR] Error: Assertion failed (Assert.mls:18) +//β”‚ ═══[RUNTIME ERROR] Error: Assertion failed (Assert.mls:15) assert xs is Array @@ -25,12 +22,9 @@ assert xs is Array :fixme assert(true); 123 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ if (123 === true) { -//β”‚ return runtime.Unit -//β”‚ } -//β”‚ return runtime.safeCall(runtime.assertFail("Assert.mls", "27")); +//β”‚ if (123 !== true) { runtime.safeCall(runtime.assertFail("Assert.mls", "24")); } //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ ═══[RUNTIME ERROR] Error: Assertion failed (Assert.mls:27) +//β”‚ ═══[RUNTIME ERROR] Error: Assertion failed (Assert.mls:24) // * FIXME: the `else` is parsed as part of the `assert`! :sjs @@ -39,13 +33,7 @@ if 1 is 2 then assert true else 3 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ if (1 === 2) { -//β”‚ if (true === true) { -//β”‚ return runtime.Unit -//β”‚ } -//β”‚ return 3; -//β”‚ } -//β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")); +//β”‚ if (1 !== 2) { throw globalThis.Object.freeze(new globalThis.Error("match error")) } //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ ═══[RUNTIME ERROR] Error: match error diff --git a/hkmc2/shared/src/test/mlscript/basics/BadDefs.mls b/hkmc2/shared/src/test/mlscript/basics/BadDefs.mls index 865eaadc2a..e1925cd891 100644 --- a/hkmc2/shared/src/test/mlscript/basics/BadDefs.mls +++ b/hkmc2/shared/src/test/mlscript/basics/BadDefs.mls @@ -22,7 +22,7 @@ val ++ = 0 :sjs ++ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return $_$_ +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 0 @@ -54,7 +54,7 @@ fun ++ z = 0 :sjs ++ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return 0 +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 0 diff --git a/hkmc2/shared/src/test/mlscript/basics/BadMemberProjections.mls b/hkmc2/shared/src/test/mlscript/basics/BadMemberProjections.mls index 74ba5ed90c..1b983067fc 100644 --- a/hkmc2/shared/src/test/mlscript/basics/BadMemberProjections.mls +++ b/hkmc2/shared/src/test/mlscript/basics/BadMemberProjections.mls @@ -14,10 +14,7 @@ //β”‚ ╙── ^ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ let lambda; -//β”‚ lambda = (undefined, function (self, ...args) { -//β”‚ return runtime.safeCall(self.x(...args)) -//β”‚ }); -//β”‚ return lambda +//β”‚ lambda = (undefined, function (self, ...args) { return runtime.safeCall(self.x(...args)) }); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = fun @@ -26,12 +23,12 @@ :re 1::x() //β”‚ ╔══[COMPILATION ERROR] Integer literal is not a known class. -//β”‚ β•‘ l.27: 1::x() +//β”‚ β•‘ l.24: 1::x() //β”‚ β•‘ ^ //β”‚ β•Ÿβ”€β”€ Note: any expression of the form `β€Ήexpressionβ€Ί::β€Ήidentifierβ€Ί` is a member projection; //β”‚ ╙── add a space before β€Ήidentifierβ€Ί to make it an operator application. //β”‚ ╔══[COMPILATION ERROR] Expected a statically known class; found integer literal. -//β”‚ β•‘ l.27: 1::x() +//β”‚ β•‘ l.24: 1::x() //β”‚ ╙── ^ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (sanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ let lambda1; @@ -58,24 +55,24 @@ let x = 1 :e "A"::x //β”‚ ╔══[COMPILATION ERROR] String literal is not a known class. -//β”‚ β•‘ l.59: "A"::x +//β”‚ β•‘ l.56: "A"::x //β”‚ β•‘ ^^^ //β”‚ β•Ÿβ”€β”€ Note: any expression of the form `β€Ήexpressionβ€Ί::β€Ήidentifierβ€Ί` is a member projection; //β”‚ ╙── add a space before β€Ήidentifierβ€Ί to make it an operator application. //β”‚ ╔══[COMPILATION ERROR] Expected a statically known class; found string literal. -//β”‚ β•‘ l.59: "A"::x +//β”‚ β•‘ l.56: "A"::x //β”‚ ╙── ^^^ //β”‚ = fun :e "A" ::x //β”‚ ╔══[COMPILATION ERROR] String literal is not a known class. -//β”‚ β•‘ l.71: "A" ::x +//β”‚ β•‘ l.68: "A" ::x //β”‚ β•‘ ^^^ //β”‚ β•Ÿβ”€β”€ Note: any expression of the form `β€Ήexpressionβ€Ί::β€Ήidentifierβ€Ί` is a member projection; //β”‚ ╙── add a space before β€Ήidentifierβ€Ί to make it an operator application. //β”‚ ╔══[COMPILATION ERROR] Expected a statically known class; found string literal. -//β”‚ β•‘ l.71: "A" ::x +//β”‚ β•‘ l.68: "A" ::x //β”‚ ╙── ^^^ //β”‚ = fun diff --git a/hkmc2/shared/src/test/mlscript/basics/BadParams.mls b/hkmc2/shared/src/test/mlscript/basics/BadParams.mls index 44d1cb7c85..5f7c6e6f58 100644 --- a/hkmc2/shared/src/test/mlscript/basics/BadParams.mls +++ b/hkmc2/shared/src/test/mlscript/basics/BadParams.mls @@ -45,7 +45,7 @@ fun f(val x) = x :sjs (x, x) => x //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let lambda4; lambda4 = (undefined, function (x, x1) { return x1 }); return lambda4 +//β”‚ let lambda4; lambda4 = (undefined, function (x, x1) { return x1 }); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = fun diff --git a/hkmc2/shared/src/test/mlscript/basics/Declare.mls b/hkmc2/shared/src/test/mlscript/basics/Declare.mls index 1cf22cbdcd..7e4a80ccf2 100644 --- a/hkmc2/shared/src/test/mlscript/basics/Declare.mls +++ b/hkmc2/shared/src/test/mlscript/basics/Declare.mls @@ -11,7 +11,7 @@ declare fun foo: Int :sjs foo //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return globalThis.foo +//β”‚ globalThis.foo; //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” diff --git a/hkmc2/shared/src/test/mlscript/basics/Drop.mls b/hkmc2/shared/src/test/mlscript/basics/Drop.mls index c335e9c090..eea810ba10 100644 --- a/hkmc2/shared/src/test/mlscript/basics/Drop.mls +++ b/hkmc2/shared/src/test/mlscript/basics/Drop.mls @@ -4,7 +4,7 @@ :sjs drop id(2 + 2) //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ Predef.id(4); return runtime.Unit +//β”‚ Predef.id(4); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” @@ -19,8 +19,9 @@ drop { a: 0, b: 1 } //β”‚ set tmp1 = { "b": b }; //β”‚ return runtime⁰.Unit⁰ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return runtime.Unit +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” +//β”‚ :sjs let discard = drop _ diff --git a/hkmc2/shared/src/test/mlscript/basics/DynamicFields.mls b/hkmc2/shared/src/test/mlscript/basics/DynamicFields.mls index 9d457f2690..496d4c5fbb 100644 --- a/hkmc2/shared/src/test/mlscript/basics/DynamicFields.mls +++ b/hkmc2/shared/src/test/mlscript/basics/DynamicFields.mls @@ -7,14 +7,14 @@ let xs = mut [1, 2, 3] :sjs xs.(0) //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return xs[0] +//β”‚ xs[0]; //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 1 :sjs set xs.(0) = 4 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ xs[0] = 4; return runtime.Unit +//β”‚ xs[0] = 4; //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” xs.(0) diff --git a/hkmc2/shared/src/test/mlscript/basics/DynamicInstantiation.mls b/hkmc2/shared/src/test/mlscript/basics/DynamicInstantiation.mls index 3f7c39daf4..8e52222579 100644 --- a/hkmc2/shared/src/test/mlscript/basics/DynamicInstantiation.mls +++ b/hkmc2/shared/src/test/mlscript/basics/DynamicInstantiation.mls @@ -6,7 +6,7 @@ class C :sjs new! C //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return globalThis.Object.freeze(new C1()) +//β”‚ globalThis.Object.freeze(new C1()); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = C @@ -39,7 +39,7 @@ new! id(C) //β”‚ rhs = Tup of Ls of //β”‚ Ident of "C" //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return globalThis.Object.freeze(new Predef.id(C1)) +//β”‚ globalThis.Object.freeze(new Predef.id(C1)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ ═══[RUNTIME ERROR] TypeError: Predef.id is not a constructor diff --git a/hkmc2/shared/src/test/mlscript/basics/DynamicSelection.mls b/hkmc2/shared/src/test/mlscript/basics/DynamicSelection.mls index bbe04bc183..a2d21e8e37 100644 --- a/hkmc2/shared/src/test/mlscript/basics/DynamicSelection.mls +++ b/hkmc2/shared/src/test/mlscript/basics/DynamicSelection.mls @@ -47,7 +47,7 @@ r ! a :sjs !(r) //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return r.value +//β”‚ r.value; //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 2 @@ -92,7 +92,7 @@ t.01 :sjs (!) //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let lambda; lambda = (undefined, function (arg) { return ! arg }); return lambda +//β”‚ let lambda; lambda = (undefined, function (arg) { return ! arg }); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = fun diff --git a/hkmc2/shared/src/test/mlscript/basics/FunnyRecordKeys.mls b/hkmc2/shared/src/test/mlscript/basics/FunnyRecordKeys.mls index 7a7f06e138..3fe3ec97a8 100644 --- a/hkmc2/shared/src/test/mlscript/basics/FunnyRecordKeys.mls +++ b/hkmc2/shared/src/test/mlscript/basics/FunnyRecordKeys.mls @@ -6,41 +6,41 @@ { a: 1 } //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return globalThis.Object.freeze({ a: 1 }) +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = {a: 1} { "a": 1 } //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return globalThis.Object.freeze({ a: 1 }) +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = {a: 1} :sjs { " ": 1 } //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return globalThis.Object.freeze({ " ": 1 }) +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = {" ": 1} :sjs { 0: 1 } //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return globalThis.Object.freeze({ 0: 1 }) +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = {"0": 1} :sjs { (0): 1 } //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return globalThis.Object.freeze({ 0: 1 }) +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = {"0": 1} :sjs { (id(0)): 1 } //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let tmp; tmp = Predef.id(0); return globalThis.Object.freeze({ [tmp]: 1 }) +//β”‚ let tmp; tmp = Predef.id(0); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = {"0": 1} diff --git a/hkmc2/shared/src/test/mlscript/basics/MemberProjections.mls b/hkmc2/shared/src/test/mlscript/basics/MemberProjections.mls index 143c9bacc3..c5583d9919 100644 --- a/hkmc2/shared/src/test/mlscript/basics/MemberProjections.mls +++ b/hkmc2/shared/src/test/mlscript/basics/MemberProjections.mls @@ -147,7 +147,7 @@ Foo::n(foo, 2) //β”‚ lambda2 = (undefined, function (self, ...args7) { //β”‚ return runtime.safeCall(self.n(...args7)) //β”‚ }); -//β”‚ return runtime.safeCall(lambda2(foo, 2)) +//β”‚ runtime.safeCall(lambda2(foo, 2)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 125 diff --git a/hkmc2/shared/src/test/mlscript/basics/MiscArrayTests.mls b/hkmc2/shared/src/test/mlscript/basics/MiscArrayTests.mls index 10d505694e..60b1145e27 100644 --- a/hkmc2/shared/src/test/mlscript/basics/MiscArrayTests.mls +++ b/hkmc2/shared/src/test/mlscript/basics/MiscArrayTests.mls @@ -55,11 +55,9 @@ xs \ //β”‚ ╙── ^^^^^^^^^^^^ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ let lambda5, tmp1; -//β”‚ lambda5 = (undefined, function (x) { -//β”‚ return x * 2 -//β”‚ }); +//β”‚ lambda5 = (undefined, function (x) { return x * 2 }); //β”‚ tmp1 = map(lambda5); -//β”‚ return Predef.passTo(xs1, tmp1) +//β”‚ Predef.passTo(xs1, tmp1); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ ═══[RUNTIME ERROR] Error: Function 'map' expected 2 arguments but got 1 @@ -68,7 +66,7 @@ xs \ xs\ map(x => x * 2) //β”‚ ╔══[COMPILATION ERROR] Expected 2 arguments, got 1 -//β”‚ β•‘ l.69: map(x => x * 2) +//β”‚ β•‘ l.67: map(x => x * 2) //β”‚ ╙── ^^^^^^^^^^^^ //β”‚ ═══[RUNTIME ERROR] Error: Function 'map' expected 2 arguments but got 1 diff --git a/hkmc2/shared/src/test/mlscript/basics/MultiParamLists.mls b/hkmc2/shared/src/test/mlscript/basics/MultiParamLists.mls index 7500c8d032..48fb69c0da 100644 --- a/hkmc2/shared/src/test/mlscript/basics/MultiParamLists.mls +++ b/hkmc2/shared/src/test/mlscript/basics/MultiParamLists.mls @@ -11,7 +11,7 @@ fun f(n1: Int): Int = n1 f(42) //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return 42 +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 42 @@ -29,7 +29,7 @@ fun f(n1: Int)(n2: Int): Int = (10 * n1 + n2) f(4)(2) //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return 42 +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 42 @@ -54,7 +54,7 @@ fun f(n1: Int)(n2: Int)(n3: Int): Int = 10 * (10 * n1 + n2) + n3 f(4)(2)(0) //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return 420 +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 420 @@ -83,7 +83,7 @@ fun f(n1: Int)(n2: Int)(n3: Int)(n4: Int): Int = 10 * (10 * (10 * n1 + n2) + n3) f(3)(0)(3)(1) //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return 3031 +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 3031 diff --git a/hkmc2/shared/src/test/mlscript/basics/MultilineExpressions.mls b/hkmc2/shared/src/test/mlscript/basics/MultilineExpressions.mls index ca54d78305..1c6983a063 100644 --- a/hkmc2/shared/src/test/mlscript/basics/MultilineExpressions.mls +++ b/hkmc2/shared/src/test/mlscript/basics/MultilineExpressions.mls @@ -80,7 +80,7 @@ if 1 + 2 //β”‚ kw = Keywrd of keyword 'then' //β”‚ rhs = IntLit of 0 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ if (7 === true) { return 0 } throw globalThis.Object.freeze(new globalThis.Error("match error")); +//β”‚ if (7 !== true) { throw globalThis.Object.freeze(new globalThis.Error("match error")) } //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ ═══[RUNTIME ERROR] Error: match error diff --git a/hkmc2/shared/src/test/mlscript/basics/MutVal.mls b/hkmc2/shared/src/test/mlscript/basics/MutVal.mls index c5eee2532a..6823b723cc 100644 --- a/hkmc2/shared/src/test/mlscript/basics/MutVal.mls +++ b/hkmc2/shared/src/test/mlscript/basics/MutVal.mls @@ -7,7 +7,7 @@ let cached = 1 :sjs set cached = 2 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ cached = 2; return runtime.Unit +//β”‚ cached = 2; //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” cached diff --git a/hkmc2/shared/src/test/mlscript/basics/PureTermStatements.mls b/hkmc2/shared/src/test/mlscript/basics/PureTermStatements.mls index 42619293f7..51d350ef83 100644 --- a/hkmc2/shared/src/test/mlscript/basics/PureTermStatements.mls +++ b/hkmc2/shared/src/test/mlscript/basics/PureTermStatements.mls @@ -85,7 +85,7 @@ fun foo() = :sjs 1; id(2) //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return Predef.id(2) +//β”‚ Predef.id(2); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 2 diff --git a/hkmc2/shared/src/test/mlscript/basics/ShortcircuitingOps.mls b/hkmc2/shared/src/test/mlscript/basics/ShortcircuitingOps.mls index 6504910bef..f87f0511e7 100644 --- a/hkmc2/shared/src/test/mlscript/basics/ShortcircuitingOps.mls +++ b/hkmc2/shared/src/test/mlscript/basics/ShortcircuitingOps.mls @@ -4,7 +4,7 @@ :sjs 1 && 2 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ if (1 === true) { return 2 } return false; +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = false @@ -29,14 +29,14 @@ false && loudTrue :sjs print(1 && loudTrue) //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ if (1 === true) { Predef.print(true); return Predef.print(true) } return Predef.print(false); +//β”‚ if (1 === true) { Predef.print(true); Predef.print(true); } else { Predef.print(false); } //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ > false :sjs 1 || 2 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ if (1 === false) { return 2 } return true; +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = true @@ -73,7 +73,7 @@ fold(||)(0, false, 42, 123) //β”‚ return true; //β”‚ }); //β”‚ callPrefix = runtime.safeCall(Predef.fold(lambda)); -//β”‚ return runtime.safeCall(callPrefix(0, false, 42, 123)) +//β”‚ runtime.safeCall(callPrefix(0, false, 42, 123)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = true diff --git a/hkmc2/shared/src/test/mlscript/basics/StrTest.mls b/hkmc2/shared/src/test/mlscript/basics/StrTest.mls index 1722f0d3da..d706a2bee1 100644 --- a/hkmc2/shared/src/test/mlscript/basics/StrTest.mls +++ b/hkmc2/shared/src/test/mlscript/basics/StrTest.mls @@ -8,7 +8,7 @@ open StrOps :sjs "a" is Str //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ if (typeof "a" === 'string') { return true } return false; +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = true diff --git a/hkmc2/shared/src/test/mlscript/basics/Underscores.mls b/hkmc2/shared/src/test/mlscript/basics/Underscores.mls index cc1a77315f..4eb84c348a 100644 --- a/hkmc2/shared/src/test/mlscript/basics/Underscores.mls +++ b/hkmc2/shared/src/test/mlscript/basics/Underscores.mls @@ -4,7 +4,7 @@ :sjs _ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let lambda; lambda = (undefined, function (_0) { return _0 }); return lambda +//β”‚ let lambda; lambda = (undefined, function (_0) { return _0 }); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = fun @@ -37,7 +37,7 @@ inc(2) :sjs _ + _ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let lambda4; lambda4 = (undefined, function (_0, _1) { return _0 + _1 }); return lambda4 +//β”‚ let lambda4; lambda4 = (undefined, function (_0, _1) { return _0 + _1 }); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = fun @@ -102,7 +102,6 @@ _ is Int //β”‚ if (globalThis.Number.isInteger(_0)) { return true } //β”‚ return false; //β”‚ }); -//β”‚ return lambda8 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = fun @@ -139,7 +138,7 @@ mkObj(1, 2) :re let mkObj = x: _, y: _, z: 3 //β”‚ ╔══[COMPILATION ERROR] Illegal position for '_' placeholder. -//β”‚ β•‘ l.140: let mkObj = x: _, y: _, z: 3 +//β”‚ β•‘ l.139: let mkObj = x: _, y: _, z: 3 //β”‚ ╙── ^ //β”‚ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. //β”‚ mkObj = fun mkObj diff --git a/hkmc2/shared/src/test/mlscript/codegen/BadNew.mls b/hkmc2/shared/src/test/mlscript/codegen/BadNew.mls index 3f7703c54f..1ab62db50f 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/BadNew.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/BadNew.mls @@ -31,7 +31,7 @@ new 2 + 2 :re new! 2 + 2 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let tmp1; tmp1 = globalThis.Object.freeze(new 2()); return tmp1 + 2 +//β”‚ let tmp1; tmp1 = globalThis.Object.freeze(new 2()); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ ═══[RUNTIME ERROR] TypeError: 2 is not a constructor diff --git a/hkmc2/shared/src/test/mlscript/codegen/BadOpen.mls b/hkmc2/shared/src/test/mlscript/codegen/BadOpen.mls index 9bb0c5e49e..e024caf37c 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/BadOpen.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/BadOpen.mls @@ -35,7 +35,7 @@ open Foo { y } :sjs y //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return Foo1.y +//β”‚ Foo1.y; //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” diff --git a/hkmc2/shared/src/test/mlscript/codegen/BasicTerms.mls b/hkmc2/shared/src/test/mlscript/codegen/BasicTerms.mls index 9ee03eeac2..d24fb964c2 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/BasicTerms.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/BasicTerms.mls @@ -6,7 +6,7 @@ 1 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return 1 +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 1 @@ -21,14 +21,15 @@ //β”‚ Program: //β”‚ main = Return of Lit of IntLit of 2 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return 2 +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 2 +//β”‚ print("Hi") //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return Predef.print("Hi") +//β”‚ Predef.print("Hi"); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ > Hi @@ -51,7 +52,7 @@ print("Hi") //β”‚ value = Lit of StrLit of "Hi" //β”‚ rest = Return of Lit of IntLit of 2 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ Predef.print("Hi"); return 2 +//β”‚ Predef.print("Hi"); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ > Hi //β”‚ = 2 @@ -60,7 +61,7 @@ print("Hi") :re 2(2) //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return runtime.safeCall(2(2)) +//β”‚ runtime.safeCall(2(2)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ ═══[RUNTIME ERROR] TypeError: 2 is not a function diff --git a/hkmc2/shared/src/test/mlscript/codegen/BlockPrinter.mls b/hkmc2/shared/src/test/mlscript/codegen/BlockPrinter.mls index 4b2aa99b4f..9c74657eec 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/BlockPrinter.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/BlockPrinter.mls @@ -65,7 +65,7 @@ x + 1 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Lowered IR |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ define xΒ² as val xΒ³ = 1; return +⁰(xΒ³, 1) //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let x2; x2 = 1; return x2 + 1 +//β”‚ let x2; x2 = 1; //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 2 //β”‚ x = 1 diff --git a/hkmc2/shared/src/test/mlscript/codegen/BuiltinOps.mls b/hkmc2/shared/src/test/mlscript/codegen/BuiltinOps.mls index 9e8fa71e8c..38435ad8de 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/BuiltinOps.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/BuiltinOps.mls @@ -4,21 +4,21 @@ :sjs 2 + 2 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return 4 +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 4 :sjs 2 +. 2 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return 4 +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 4 :sjs +2 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return 2 +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 2 @@ -82,7 +82,7 @@ id(+)(1) //β”‚ return arg1 + arg2 //β”‚ }); //β”‚ baseCall1 = Predef.id(lambda4); -//β”‚ return runtime.safeCall(baseCall1(1)) +//β”‚ runtime.safeCall(baseCall1(1)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ ═══[RUNTIME ERROR] Error: Function expected 2 arguments but got 1 @@ -92,7 +92,7 @@ fun (+) lol(a, b) = [a, b] :sjs 1 + 2 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return globalThis.Object.freeze([ 1, 2 ]) +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = [1, 2] @@ -105,7 +105,7 @@ id(~)(2) //β”‚ return ~ arg //β”‚ }); //β”‚ baseCall2 = Predef.id(lambda5); -//β”‚ return runtime.safeCall(baseCall2(2)) +//β”‚ runtime.safeCall(baseCall2(2)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = -3 diff --git a/hkmc2/shared/src/test/mlscript/codegen/CaseShorthand.mls b/hkmc2/shared/src/test/mlscript/codegen/CaseShorthand.mls index 242980fe8d..bd68ead8a0 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/CaseShorthand.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/CaseShorthand.mls @@ -7,7 +7,7 @@ case x then x :sjs case { x then x } //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let lambda1; lambda1 = (undefined, function (caseScrut) { return caseScrut }); return lambda1 +//β”‚ let lambda1; lambda1 = (undefined, function (caseScrut) { return caseScrut }); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = fun @@ -22,7 +22,6 @@ x => if x is //β”‚ } //β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")); //β”‚ }); -//β”‚ return lambda2 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = fun @@ -36,7 +35,6 @@ case 0 then true //β”‚ } //β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")); //β”‚ }); -//β”‚ return lambda3 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = fun @@ -48,13 +46,9 @@ case //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ let lambda4; //β”‚ lambda4 = (undefined, function (caseScrut) { -//β”‚ if (caseScrut === 0) { -//β”‚ Predef.print(1); -//β”‚ return runtime.Unit -//β”‚ } +//β”‚ if (caseScrut === 0) { Predef.print(1); return runtime.Unit } //β”‚ return runtime.Unit; //β”‚ }); -//β”‚ return lambda4 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = fun @@ -77,10 +71,10 @@ case :todo // TODO: support this braceless syntax? case [x] then x, [] then 0 //β”‚ ╔══[COMPILATION ERROR] Unexpected infix use of keyword 'then' here -//β”‚ β•‘ l.78: case [x] then x, [] then 0 +//β”‚ β•‘ l.72: case [x] then x, [] then 0 //β”‚ ╙── ^^^^^^^^^ //β”‚ ╔══[WARNING] Pure expression in statement position -//β”‚ β•‘ l.78: case [x] then x, [] then 0 +//β”‚ β•‘ l.72: case [x] then x, [] then 0 //β”‚ ╙── ^^^^^^^^^^^^^^^ //β”‚ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. @@ -90,11 +84,7 @@ case _ then false //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ let lambda10; -//β”‚ lambda10 = (undefined, function (caseScrut) { -//β”‚ if (caseScrut === 0) { return true } -//β”‚ return false; -//β”‚ }); -//β”‚ return lambda10 +//β”‚ lambda10 = (undefined, function (caseScrut) { if (caseScrut === 0) { return true } return false; }); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = fun @@ -123,7 +113,7 @@ val isDefined = case :todo case 0 | 1 //β”‚ ╔══[COMPILATION ERROR] Unrecognized pattern split (infix operator '|'). -//β”‚ β•‘ l.124: case 0 | 1 +//β”‚ β•‘ l.114: case 0 | 1 //β”‚ ╙── ^^^^^ //β”‚ = fun diff --git a/hkmc2/shared/src/test/mlscript/codegen/ClassMatching.mls b/hkmc2/shared/src/test/mlscript/codegen/ClassMatching.mls index 4e9b5d1f24..2fa1f45116 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/ClassMatching.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/ClassMatching.mls @@ -11,10 +11,9 @@ if Some(0) is Some(x) then x //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ let scrut; //β”‚ scrut = Some1(0); -//β”‚ if (scrut instanceof Some1.class) { -//β”‚ return scrut.value +//β”‚ if (scrut instanceof Some1.class) {} else { +//β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")) //β”‚ } -//β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 0 @@ -26,10 +25,9 @@ let s = Some(0) if s is Some(x) then x //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ if (s instanceof Some1.class) { -//β”‚ return s.value +//β”‚ if (s instanceof Some1.class) {} else { +//β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")) //β”‚ } -//β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 0 @@ -95,7 +93,6 @@ x => if x is Some(x) then x //β”‚ } //β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")); //β”‚ }); -//β”‚ return lambda //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = fun diff --git a/hkmc2/shared/src/test/mlscript/codegen/Comma.mls b/hkmc2/shared/src/test/mlscript/codegen/Comma.mls index faf901b723..439b531315 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/Comma.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/Comma.mls @@ -17,7 +17,7 @@ fun f() = { console.log("ok"), 42 } fun f() = console.log("ok"), 42 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let f2; f2 = function f() { return runtime.safeCall(globalThis.console.log("ok")) }; return 42 +//β”‚ let f2; f2 = function f() { return runtime.safeCall(globalThis.console.log("ok")) }; //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 42 diff --git a/hkmc2/shared/src/test/mlscript/codegen/ConsoleLog.mls b/hkmc2/shared/src/test/mlscript/codegen/ConsoleLog.mls index 1d3ef73438..b766f1900e 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/ConsoleLog.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/ConsoleLog.mls @@ -15,9 +15,7 @@ console.log("a") console.log("b") 123 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ runtime.safeCall(globalThis.console.log("a")); -//β”‚ runtime.safeCall(globalThis.console.log("b")); -//β”‚ return 123 +//β”‚ runtime.safeCall(globalThis.console.log("a")); runtime.safeCall(globalThis.console.log("b")); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ > a //β”‚ > b @@ -26,21 +24,21 @@ console.log("b") let l = console.log l(123) //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let l; l = globalThis.console.log; return runtime.safeCall(l(123)) +//β”‚ let l; l = globalThis.console.log; runtime.safeCall(l(123)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ > 123 //β”‚ l = fun log 42 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return 42 +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 42 console.log("a") //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return runtime.safeCall(globalThis.console.log("a")) +//β”‚ runtime.safeCall(globalThis.console.log("a")); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ > a @@ -59,7 +57,6 @@ y * 2 //β”‚ runtime.safeCall(globalThis.console.log("b")); //β”‚ y = 124; //β”‚ runtime.safeCall(globalThis.console.log("c")); -//β”‚ return 248 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ > a //β”‚ > b diff --git a/hkmc2/shared/src/test/mlscript/codegen/CurriedClasses.mls b/hkmc2/shared/src/test/mlscript/codegen/CurriedClasses.mls index 301fa0d67e..07f028cb15 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/CurriedClasses.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/CurriedClasses.mls @@ -44,7 +44,7 @@ h.y :expect 2 A(1)(2).y //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let tmp; tmp = globalThis.Object.freeze(new A1.class(1, 2)); return tmp.y +//β”‚ let tmp; tmp = globalThis.Object.freeze(new A1.class(1, 2)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 2 @@ -71,7 +71,6 @@ new A(...[4])(...[5]).y //β”‚ 5 //β”‚ ]); //β”‚ tmp3 = globalThis.Object.freeze(new A1.class(...tmp1, ...tmp2)); -//β”‚ return tmp3.y //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 5 @@ -106,22 +105,22 @@ h.y class A(x1, x2, ...xs)(y1, ...ys) with fun fields = [x1, x2, xs, y1, ys] //β”‚ ╔══[COMPILATION ERROR] Spread parameters are not supported in class parameters. -//β”‚ β•‘ l.106: class A(x1, x2, ...xs)(y1, ...ys) with +//β”‚ β•‘ l.105: class A(x1, x2, ...xs)(y1, ...ys) with //β”‚ ╙── ^^ //β”‚ ╔══[COMPILATION ERROR] Spread parameters are not supported in class parameters. -//β”‚ β•‘ l.106: class A(x1, x2, ...xs)(y1, ...ys) with +//β”‚ β•‘ l.105: class A(x1, x2, ...xs)(y1, ...ys) with //β”‚ ╙── ^^ //β”‚ ╔══[COMPILATION ERROR] No definition found in scope for member 'xs' -//β”‚ β•‘ l.107: fun fields = [x1, x2, xs, y1, ys] +//β”‚ β•‘ l.106: fun fields = [x1, x2, xs, y1, ys] //β”‚ β•‘ ^^ //β”‚ β•Ÿβ”€β”€ which references the symbol introduced here -//β”‚ β•‘ l.106: class A(x1, x2, ...xs)(y1, ...ys) with +//β”‚ β•‘ l.105: class A(x1, x2, ...xs)(y1, ...ys) with //β”‚ ╙── ^^ //β”‚ ╔══[COMPILATION ERROR] No definition found in scope for member 'ys' -//β”‚ β•‘ l.107: fun fields = [x1, x2, xs, y1, ys] +//β”‚ β•‘ l.106: fun fields = [x1, x2, xs, y1, ys] //β”‚ β•‘ ^^ //β”‚ β•Ÿβ”€β”€ which references the symbol introduced here -//β”‚ β•‘ l.106: class A(x1, x2, ...xs)(y1, ...ys) with +//β”‚ β•‘ l.105: class A(x1, x2, ...xs)(y1, ...ys) with //β”‚ ╙── ^^ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ let A3; @@ -155,16 +154,16 @@ class A(x1, x2, ...xs)(y1, ...ys) with //β”‚ static [definitionMetadata] = ["class", "A", [null, null]]; //β”‚ }); //β”‚ ╔══[COMPILATION ERROR] No definition found in scope for member 'xs' -//β”‚ β•‘ l.107: fun fields = [x1, x2, xs, y1, ys] +//β”‚ β•‘ l.106: fun fields = [x1, x2, xs, y1, ys] //β”‚ β•‘ ^^ //β”‚ β•Ÿβ”€β”€ which references the symbol introduced here -//β”‚ β•‘ l.106: class A(x1, x2, ...xs)(y1, ...ys) with +//β”‚ β•‘ l.105: class A(x1, x2, ...xs)(y1, ...ys) with //β”‚ ╙── ^^ //β”‚ ╔══[COMPILATION ERROR] No definition found in scope for member 'ys' -//β”‚ β•‘ l.107: fun fields = [x1, x2, xs, y1, ys] +//β”‚ β•‘ l.106: fun fields = [x1, x2, xs, y1, ys] //β”‚ β•‘ ^^ //β”‚ β•Ÿβ”€β”€ which references the symbol introduced here -//β”‚ β•‘ l.106: class A(x1, x2, ...xs)(y1, ...ys) with +//β”‚ β•‘ l.105: class A(x1, x2, ...xs)(y1, ...ys) with //β”‚ ╙── ^^ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” diff --git a/hkmc2/shared/src/test/mlscript/codegen/Formatting.mls b/hkmc2/shared/src/test/mlscript/codegen/Formatting.mls index 673fc1c5cf..31683c8e7f 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/Formatting.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/Formatting.mls @@ -17,27 +17,25 @@ let discard = drop _ discard of { a: 0, b: 11111 } //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let tmp; tmp = globalThis.Object.freeze({ a: 0, b: 11111 }); return runtime.safeCall(discard(tmp)) +//β”‚ let tmp; tmp = globalThis.Object.freeze({ a: 0, b: 11111 }); runtime.safeCall(discard(tmp)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” discard of { a: 0, b: 11111 } //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let tmp1; -//β”‚ tmp1 = globalThis.Object.freeze({ a: 0, b: 11111 }); -//β”‚ return runtime.safeCall(discard(tmp1)) +//β”‚ let tmp1; tmp1 = globalThis.Object.freeze({ a: 0, b: 11111 }); runtime.safeCall(discard(tmp1)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” discard of { aaaa: 1 } //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let tmp2; tmp2 = globalThis.Object.freeze({ aaaa: 1 }); return runtime.safeCall(discard(tmp2)) +//β”‚ let tmp2; tmp2 = globalThis.Object.freeze({ aaaa: 1 }); runtime.safeCall(discard(tmp2)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” discard of { aaaaaaaaa: 1, bbbb: 2 } //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ let tmp3; //β”‚ tmp3 = globalThis.Object.freeze({ aaaaaaaaa: 1, bbbb: 2 }); -//β”‚ return runtime.safeCall(discard(tmp3)) +//β”‚ runtime.safeCall(discard(tmp3)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” discard of { aaaaaaaaa: 1, bbbbbbbb: 2, ccccccccc: 3, dddddddddd: 4 } @@ -49,7 +47,7 @@ discard of { aaaaaaaaa: 1, bbbbbbbb: 2, ccccccccc: 3, dddddddddd: 4 } //β”‚ ccccccccc: 3, //β”‚ dddddddddd: 4 //β”‚ }); -//β”‚ return runtime.safeCall(discard(tmp4)) +//β”‚ runtime.safeCall(discard(tmp4)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” @@ -60,7 +58,7 @@ discard of [ "aaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbb" ] //β”‚ "aaaaaaaaaaaaaaa", //β”‚ "bbbbbbbbbbbbbbbb" //β”‚ ]); -//β”‚ return runtime.safeCall(discard(tmp5)) +//β”‚ runtime.safeCall(discard(tmp5)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” discard of [ "aaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbb", "ccccccccccccccccc", "dddddddddddddddddd" ] @@ -72,7 +70,7 @@ discard of [ "aaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbb", "ccccccccccccccccc", "dddddd //β”‚ "ccccccccccccccccc", //β”‚ "dddddddddddddddddd" //β”‚ ]); -//β”‚ return runtime.safeCall(discard(tmp6)) +//β”‚ runtime.safeCall(discard(tmp6)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” discard of [[ "aaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbb", "ccccccccccccccccc", "dddddddddddddddddd" ]] @@ -85,7 +83,7 @@ discard of [[ "aaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbb", "ccccccccccccccccc", "ddddd //β”‚ "dddddddddddddddddd" //β”‚ ]); //β”‚ tmp8 = globalThis.Object.freeze([ tmp7 ]); -//β”‚ return runtime.safeCall(discard(tmp8)) +//β”‚ runtime.safeCall(discard(tmp8)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” diff --git a/hkmc2/shared/src/test/mlscript/codegen/FunInClass.mls b/hkmc2/shared/src/test/mlscript/codegen/FunInClass.mls index b4eeaaa5f4..aff96ae204 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/FunInClass.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/FunInClass.mls @@ -166,7 +166,7 @@ Foo(123) //β”‚ toString() { return runtime.render(this); } //β”‚ static [definitionMetadata] = ["class", "Foo", ["a"]]; //β”‚ }); -//β”‚ return Foo1(123) +//β”‚ Foo1(123); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = Foo(123) @@ -199,6 +199,6 @@ Bar(1) //β”‚ toString() { return runtime.render(this); } //β”‚ static [definitionMetadata] = ["class", "Bar", ["x"]]; //β”‚ }); -//β”‚ return Bar1(1) +//β”‚ Bar1(1); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = Bar(1) diff --git a/hkmc2/shared/src/test/mlscript/codegen/Getters.mls b/hkmc2/shared/src/test/mlscript/codegen/Getters.mls index f7791cbcaa..8a977671b6 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/Getters.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/Getters.mls @@ -13,7 +13,7 @@ fun t = 42 :sjs t //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return t() +//β”‚ t(); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 42 @@ -125,7 +125,7 @@ module M with :sjs M.t //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return M1.t +//β”‚ M1.t; //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 0 diff --git a/hkmc2/shared/src/test/mlscript/codegen/GlobalThis.mls b/hkmc2/shared/src/test/mlscript/codegen/GlobalThis.mls index 44e7977a36..668e54ea24 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/GlobalThis.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/GlobalThis.mls @@ -21,10 +21,7 @@ globalThis :re if false then 0 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ if (false === true) { -//β”‚ return 0 -//β”‚ } -//β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")); +//β”‚ if (false !== true) { throw globalThis.Object.freeze(new globalThis.Error("match error")) } //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ ═══[RUNTIME ERROR] TypeError: Cannot read properties of undefined (reading 'freeze') //β”‚ ═══[RUNTIME ERROR] TypeError: Cannot read properties of undefined (reading 'freeze') @@ -44,10 +41,7 @@ foo() //β”‚ } //β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")); //β”‚ }; -//β”‚ if (false === true) { -//β”‚ return 0 -//β”‚ } -//β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")); +//β”‚ if (false !== true) { throw globalThis.Object.freeze(new globalThis.Error("match error")) } //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ ═══[RUNTIME ERROR] TypeError: Cannot read properties of undefined (reading 'freeze') //β”‚ ═══[RUNTIME ERROR] TypeError: Cannot read properties of undefined (reading 'freeze') diff --git a/hkmc2/shared/src/test/mlscript/codegen/Hygiene.mls b/hkmc2/shared/src/test/mlscript/codegen/Hygiene.mls index 78f3c79ce4..1d7a42603c 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/Hygiene.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/Hygiene.mls @@ -41,7 +41,7 @@ print(Test) :sjs Test.foo() //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return Test1.foo() +//β”‚ Test1.foo(); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ > Test { x: 12, class: object Test } //β”‚ = 12 @@ -64,7 +64,7 @@ let f = () => x let x = 2 f() //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let x, f, x1, f1; x = 1; f1 = function f() { return x }; f = f1; x1 = 2; return x +//β”‚ let x, f, x1, f1; x = 1; f1 = function f() { return x }; f = f1; x1 = 2; //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 1 //β”‚ f = fun f diff --git a/hkmc2/shared/src/test/mlscript/codegen/ImportAlias.mls b/hkmc2/shared/src/test/mlscript/codegen/ImportAlias.mls index 146759266e..05eeaebc93 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/ImportAlias.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/ImportAlias.mls @@ -1,7 +1,7 @@ :js -import "../../mlscript-compile/Example.mls" as AnotherExample +import "../../mlscript-compile/Example.mls" { Example as AnotherExample } :e Example.inc(0) @@ -16,7 +16,7 @@ AnotherExample.inc(0) :silent -import "../../mlscript-compile/Example.mjs" as JSExample +import "../../mlscript-compile/Example.mjs" { Example as JSExample } :e Example.inc(0) diff --git a/hkmc2/shared/src/test/mlscript/codegen/ImportExample.mls b/hkmc2/shared/src/test/mlscript/codegen/ImportExample.mls index 6f7acae0fa..3b391f933b 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/ImportExample.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/ImportExample.mls @@ -34,7 +34,7 @@ let n = 42 :sjs n / 2 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return n / 2 +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 21 @@ -46,7 +46,7 @@ open Example :sjs inc / 2 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return Example.funnySlash(Example.inc, 2) +//β”‚ Example.funnySlash(Example.inc, 2); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 3 @@ -54,7 +54,7 @@ inc / 2 :re n / 2 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return Example.funnySlash(n, 2) +//β”‚ Example.funnySlash(n, 2); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ ═══[RUNTIME ERROR] TypeError: f is not a function diff --git a/hkmc2/shared/src/test/mlscript/codegen/ImportMLs.mls b/hkmc2/shared/src/test/mlscript/codegen/ImportMLs.mls index 9281eb5258..dc0e7d239e 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/ImportMLs.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/ImportMLs.mls @@ -20,7 +20,7 @@ open Option :sjs None isDefined() //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return Option.isDefined(Option.None) +//β”‚ Option.isDefined(Option.None); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = false @@ -40,7 +40,7 @@ Some(1) :sjs (new Some(1)) isDefined() //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let tmp3; tmp3 = globalThis.Object.freeze(new Option.Some.class(1)); return Option.isDefined(tmp3) +//β”‚ let tmp3; tmp3 = globalThis.Object.freeze(new Option.Some.class(1)); Option.isDefined(tmp3); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = true diff --git a/hkmc2/shared/src/test/mlscript/codegen/ImportUrl.mls b/hkmc2/shared/src/test/mlscript/codegen/ImportUrl.mls new file mode 100644 index 0000000000..bd16f36045 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/codegen/ImportUrl.mls @@ -0,0 +1,4 @@ +:sjs +import "https://esm.sh/@floating-ui/dom" +import "https://esm.sh/@floating-ui/dom" { computePosition, shift } +import "https://esm.sh/@floating-ui/dom" as FloatingUI diff --git a/hkmc2/shared/src/test/mlscript/codegen/ImportedOps.mls b/hkmc2/shared/src/test/mlscript/codegen/ImportedOps.mls index 65c74b8cf5..2ff11d3620 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/ImportedOps.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/ImportedOps.mls @@ -18,7 +18,7 @@ foo() //β”‚ return M1.concat(tmp1, "c") //β”‚ }; //β”‚ tmp = M1.concat("a", "b"); -//β”‚ return M1.concat(tmp, "c") +//β”‚ M1.concat(tmp, "c"); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = "abc" diff --git a/hkmc2/shared/src/test/mlscript/codegen/InlineLambdas.mls b/hkmc2/shared/src/test/mlscript/codegen/InlineLambdas.mls index e0f8e06c10..e1cea60b56 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/InlineLambdas.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/InlineLambdas.mls @@ -13,7 +13,7 @@ //β”‚ tmp3 = tmp2 + 1; //β”‚ return tmp3 + 1 //β”‚ }); -//β”‚ return runtime.safeCall(lambda(1)) +//β”‚ runtime.safeCall(lambda(1)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 6 @@ -30,21 +30,21 @@ //β”‚ tmp4 = tmp3 + 1; //β”‚ return tmp4 + 1 //β”‚ }); -//β”‚ return runtime.safeCall(lambda1(1)) +//β”‚ runtime.safeCall(lambda1(1)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 7 :sjs (x => x) + 1 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let lambda2; lambda2 = (undefined, function (x) { return x }); return lambda2 + 1 +//β”‚ let lambda2; lambda2 = (undefined, function (x) { return x }); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = "function (x) { runtime.checkArgs(\"\", 1, true, arguments.length); return x }1" :sjs 1 + (x => x) //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let lambda3; lambda3 = (undefined, function (x) { return x }); return 1 + lambda3 +//β”‚ let lambda3; lambda3 = (undefined, function (x) { return x }); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = "1function (x) { runtime.checkArgs(\"\", 1, true, arguments.length); return x }" diff --git a/hkmc2/shared/src/test/mlscript/codegen/Inliner.mls b/hkmc2/shared/src/test/mlscript/codegen/Inliner.mls index db15d8c103..cc704291b6 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/Inliner.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/Inliner.mls @@ -162,7 +162,7 @@ f(1, 2, 3, ...[4]) //β”‚ ]); //β”‚ f7(1, 2, ...tmp6); //β”‚ tmp7 = globalThis.Object.freeze([ 4 ]); -//β”‚ return f7(1, 2, 3, ...tmp7) +//β”‚ f7(1, 2, 3, ...tmp7); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ > 3 //β”‚ > [3] @@ -182,7 +182,7 @@ fun f() = g(true) f() //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let f8; f8 = function f() { return true }; return true +//β”‚ let f8; f8 = function f() { return true }; //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = true @@ -222,7 +222,6 @@ f(1)(2)(3) //β”‚ return tmp10 * 2 //β”‚ }; //β”‚ f10 = function f(a) { return (b) => { return (c3) => { return f11(a, b, c3) } } }; -//β”‚ return 14 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 14 diff --git a/hkmc2/shared/src/test/mlscript/codegen/Lambdas.mls b/hkmc2/shared/src/test/mlscript/codegen/Lambdas.mls index 2f8c5fcfab..d885d3cec4 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/Lambdas.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/Lambdas.mls @@ -6,7 +6,7 @@ x => let y = x y //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let lambda; lambda = (undefined, function (x) { return x }); return lambda +//β”‚ let lambda; lambda = (undefined, function (x) { return x }); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = fun @@ -14,7 +14,7 @@ x => :sjs (acc, _) => acc //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let lambda1; lambda1 = (undefined, function (acc, _) { return acc }); return lambda1 +//β”‚ let lambda1; lambda1 = (undefined, function (acc, _) { return acc }); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = fun @@ -49,7 +49,6 @@ x => if x is (0 | 1) then 1 //β”‚ } //β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")) //β”‚ }); -//β”‚ return lambda4 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = fun @@ -66,7 +65,7 @@ if (foo of x => x + 1) then 1 :e if foo of x => x + 1 then 1 //β”‚ ╔══[COMPILATION ERROR] Unrecognized term split (application) -//β”‚ β•‘ l.67: if foo of x => x + 1 then 1 +//β”‚ β•‘ l.66: if foo of x => x + 1 then 1 //β”‚ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^ //β”‚ ═══[RUNTIME ERROR] Error: match error diff --git a/hkmc2/shared/src/test/mlscript/codegen/MergeMatchArms.mls b/hkmc2/shared/src/test/mlscript/codegen/MergeMatchArms.mls index 0fc8dfc3b5..1b31eb0336 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/MergeMatchArms.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/MergeMatchArms.mls @@ -22,16 +22,10 @@ if a is C then 3 D then 4 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ if (a instanceof A1.class) { -//β”‚ return 1 -//β”‚ } else if (a instanceof B1.class) { -//β”‚ return 2 -//β”‚ } else if (a instanceof C1.class) { -//β”‚ return 3 -//β”‚ } else if (a instanceof D1.class) { -//β”‚ return 4 +//β”‚ if (a instanceof A1.class) {} else if (a instanceof B1.class) {} else if (a instanceof C1.class) {} else if (a instanceof D1.class) {} else { +//β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")) //β”‚ } -//β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")); +//β”‚ /* Rest moved to non-abortive branch(es) */ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 1 @@ -45,14 +39,10 @@ if a is B then 2 C then 3 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ if (a instanceof A1.class) { -//β”‚ return 1 -//β”‚ } else if (a instanceof B1.class) { -//β”‚ return 2 -//β”‚ } else if (a instanceof C1.class) { -//β”‚ return 3 +//β”‚ if (a instanceof A1.class) {} else if (a instanceof B1.class) {} else if (a instanceof C1.class) {} else { +//β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")) //β”‚ } -//β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")); +//β”‚ /* Rest moved to non-abortive branch(es) */ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 1 @@ -66,18 +56,14 @@ if a is C then 3 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ if (a instanceof A1.class) { -//β”‚ if (b instanceof A1.class) { -//β”‚ return 1 -//β”‚ } else if (b instanceof B1.class) { -//β”‚ return 2 +//β”‚ if (b instanceof A1.class) {} else if (b instanceof B1.class) {} else { +//β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")) //β”‚ } -//β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")); -//β”‚ } else if (a instanceof B1.class) { -//β”‚ return 2 -//β”‚ } else if (a instanceof C1.class) { -//β”‚ return 3 +//β”‚ /* Rest moved to non-abortive branch(es) */ +//β”‚ } else if (a instanceof B1.class) {} else if (a instanceof C1.class) {} else { +//β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")) //β”‚ } -//β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")); +//β”‚ /* Rest moved to non-abortive branch(es) */ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 2 @@ -118,7 +104,7 @@ print(x) //β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")) //β”‚ } //β”‚ x = tmp; -//β”‚ return Predef.print(tmp) +//β”‚ Predef.print(tmp); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ > 1 //β”‚ x = 1 @@ -132,12 +118,10 @@ if a is let tmp = 2 B then 2 + tmp //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ if (a instanceof A1.class) { -//β”‚ return 1 -//β”‚ } else if (a instanceof B1.class) { -//β”‚ return 4 +//β”‚ if (a instanceof A1.class) {} else if (a instanceof B1.class) {} else { +//β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")) //β”‚ } -//β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")); +//β”‚ /* Rest moved to non-abortive branch(es) */ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 1 @@ -154,14 +138,14 @@ if a is let tmp = printAndId(3) B then 2 + tmp //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ if (a instanceof A1.class) { -//β”‚ return 1 -//β”‚ } -//β”‚ Predef.print(3); -//β”‚ if (a instanceof B1.class) { -//β”‚ return 5 +//β”‚ if (a instanceof A1.class) {} else { +//β”‚ Predef.print(3); +//β”‚ if (a instanceof B1.class) {} else { +//β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")) +//β”‚ } +//β”‚ /* Rest moved to non-abortive branch(es) */ //β”‚ } -//β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")); +//β”‚ /* Rest moved to non-abortive branch(es) */ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 1 @@ -177,14 +161,14 @@ if a is C then 3 print(x) //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ if (a instanceof A1.class) { -//β”‚ return 1 -//β”‚ } else if (a instanceof B1.class) { -//β”‚ return Predef.print(2) +//β”‚ if (a instanceof A1.class) {} else if (a instanceof B1.class) { +//β”‚ Predef.print(2); //β”‚ } else if (a instanceof C1.class) { -//β”‚ return Predef.print(3) +//β”‚ Predef.print(3); +//β”‚ } else { +//β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")) //β”‚ } -//β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")); +//β”‚ /* Rest moved to non-abortive branch(es) */ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 1 @@ -204,21 +188,21 @@ if a is print(x + 2) //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ let tmp1, tmp2, tmp3; -//β”‚ if (a instanceof B1.class) { -//β”‚ return 1 -//β”‚ } -//β”‚ if (a instanceof A1.class) { -//β”‚ tmp1 = 2; -//β”‚ } else if (a instanceof C1.class) { -//β”‚ tmp1 = 3; -//β”‚ } else { -//β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")) +//β”‚ if (a instanceof B1.class) {} else { +//β”‚ if (a instanceof A1.class) { +//β”‚ tmp1 = 2; +//β”‚ } else if (a instanceof C1.class) { +//β”‚ tmp1 = 3; +//β”‚ } else { +//β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")) +//β”‚ } +//β”‚ Predef.print(tmp1); +//β”‚ tmp2 = tmp1 + 1; +//β”‚ Predef.print(tmp2); +//β”‚ tmp3 = tmp1 + 2; +//β”‚ Predef.print(tmp3); //β”‚ } -//β”‚ Predef.print(tmp1); -//β”‚ tmp2 = tmp1 + 1; -//β”‚ Predef.print(tmp2); -//β”‚ tmp3 = tmp1 + 2; -//β”‚ return Predef.print(tmp3); +//β”‚ /* Rest moved to non-abortive branch(es) */ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ > 2 //β”‚ > 3 diff --git a/hkmc2/shared/src/test/mlscript/codegen/Misc.mls b/hkmc2/shared/src/test/mlscript/codegen/Misc.mls index 4d07b442cd..404e15fafe 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/Misc.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/Misc.mls @@ -22,7 +22,7 @@ 1 + 2 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return 3 +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 3 diff --git a/hkmc2/shared/src/test/mlscript/codegen/ModuleResolution.mls b/hkmc2/shared/src/test/mlscript/codegen/ModuleResolution.mls new file mode 100644 index 0000000000..f36458eed4 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/codegen/ModuleResolution.mls @@ -0,0 +1,36 @@ +:js + +// Hooray! No need to use relative paths! +import "std/Stack.mls" + +open Stack + +1 :: 2 :: 3 :: Nil +//β”‚ = Cons(1, Cons(2, Cons(3, Nil))) + +:silent +import "fs" + +fs.readdirSync +//β”‚ = fun readdirSync + +:silent +import "node:url" + +// Note that the prefix `node:` is removed by the module resolver. +url.pathToFileURL +//β”‚ = fun pathToFileURL + +// Importing packages in node_modules will be checked. + +:silent +import "typescript" + +typescript.version is Str +//β”‚ = true + +// The following will produce the expected error, but it will also display a +// local path in the error message. So I have to comment it out for now. +// :e +// :re +// import "hooray" diff --git a/hkmc2/shared/src/test/mlscript/codegen/Modules.mls b/hkmc2/shared/src/test/mlscript/codegen/Modules.mls index 0901a892cf..7318935d41 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/Modules.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/Modules.mls @@ -17,7 +17,7 @@ module None :sjs None //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return None1 +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = class None @@ -25,7 +25,7 @@ None :re None() //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return runtime.safeCall(None1()) +//β”‚ runtime.safeCall(None1()); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ ═══[RUNTIME ERROR] TypeError: Class constructor None cannot be invoked without 'new' @@ -44,7 +44,7 @@ new! None //β”‚ β•‘ l.42: new! None //β”‚ ╙── ^^^^ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return globalThis.Object.freeze(new None1()) +//β”‚ globalThis.Object.freeze(new None1()); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = None @@ -134,7 +134,7 @@ module M with :sjs M.m //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return M3.m +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = class M { m: ref'1 } as ref'1 diff --git a/hkmc2/shared/src/test/mlscript/codegen/NestedScoped.mls b/hkmc2/shared/src/test/mlscript/codegen/NestedScoped.mls index 8a1343bf25..acf39691b6 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/NestedScoped.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/NestedScoped.mls @@ -11,9 +11,10 @@ scope.locally of 42 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Lowered IR |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ return 42 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return 42 +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 42 +//β”‚ :sir @@ -57,7 +58,7 @@ scope.locally of scope.locally of ( :e x //β”‚ ╔══[COMPILATION ERROR] Name not found: x -//β”‚ β•‘ l.58: x +//β”‚ β•‘ l.59: x //β”‚ ╙── ^ //β”‚ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. @@ -153,7 +154,7 @@ fun foo(x, y) = :ge scope.locally() //β”‚ ╔══[COMPILATION ERROR] Unsupported form for scope.locally. -//β”‚ β•‘ l.154: scope.locally() +//β”‚ β•‘ l.155: scope.locally() //β”‚ ╙── ^^^^^^^^^^^^^ //β”‚ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. @@ -161,7 +162,7 @@ scope.locally() :ge scope.locally of (let x = 1 in x), (let y = 2 in y) //β”‚ ╔══[COMPILATION ERROR] Unsupported form for scope.locally. -//β”‚ β•‘ l.162: scope.locally of (let x = 1 in x), (let y = 2 in y) +//β”‚ β•‘ l.163: scope.locally of (let x = 1 in x), (let y = 2 in y) //β”‚ ╙── ^^^^^^^^^^^^^ //β”‚ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. @@ -170,10 +171,10 @@ scope.locally of (let x = 1 in x), (let y = 2 in y) :ge scope.locally of (let x = 1 in x), (let y = 2 in x) //β”‚ ╔══[COMPILATION ERROR] Name not found: x -//β”‚ β•‘ l.171: scope.locally of (let x = 1 in x), (let y = 2 in x) +//β”‚ β•‘ l.172: scope.locally of (let x = 1 in x), (let y = 2 in x) //β”‚ ╙── ^ //β”‚ ╔══[COMPILATION ERROR] Unsupported form for scope.locally. -//β”‚ β•‘ l.171: scope.locally of (let x = 1 in x), (let y = 2 in x) +//β”‚ β•‘ l.172: scope.locally of (let x = 1 in x), (let y = 2 in x) //β”‚ ╙── ^^^^^^^^^^^^^ //β”‚ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. diff --git a/hkmc2/shared/src/test/mlscript/codegen/OpenWildcard.mls b/hkmc2/shared/src/test/mlscript/codegen/OpenWildcard.mls index 4abffeeaec..9eee8d9bb0 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/OpenWildcard.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/OpenWildcard.mls @@ -16,7 +16,7 @@ open Option :sjs None isDefined() //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return Option.isDefined(Option.None) +//β”‚ Option.isDefined(Option.None); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = false @@ -61,7 +61,7 @@ val Option = "Oops" :sjs Some(123) //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return Option.Some(123) +//β”‚ Option.Some(123); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = Some(123) @@ -77,14 +77,14 @@ open Option :sjs Some //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return Option.Some +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = fun Some { class: class Some } :sjs None //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return Option3.None +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 123 diff --git a/hkmc2/shared/src/test/mlscript/codegen/ParamClasses.mls b/hkmc2/shared/src/test/mlscript/codegen/ParamClasses.mls index f1edfe839f..b562a0014e 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/ParamClasses.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/ParamClasses.mls @@ -14,6 +14,7 @@ Foo //β”‚ return FooΒΉ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = fun Foo { class: class Foo } +//β”‚ Foo() //β”‚ = Foo() @@ -56,6 +57,7 @@ data class Foo(a) Foo //β”‚ = fun Foo { class: class Foo } +//β”‚ Foo(1) //β”‚ = Foo(1) @@ -170,10 +172,10 @@ Foo(10, 20, 30, 40, 50) class Foo(val z, val z) //β”‚ ╔══[COMPILATION ERROR] Duplicate definition of member named 'z'. //β”‚ β•Ÿβ”€β”€ Defined at: -//β”‚ β•‘ l.170: class Foo(val z, val z) +//β”‚ β•‘ l.172: class Foo(val z, val z) //β”‚ β•‘ ^ //β”‚ β•Ÿβ”€β”€ Defined at: -//β”‚ β•‘ l.170: class Foo(val z, val z) +//β”‚ β•‘ l.172: class Foo(val z, val z) //β”‚ ╙── ^ Foo(1, 2) diff --git a/hkmc2/shared/src/test/mlscript/codegen/PartialApps.mls b/hkmc2/shared/src/test/mlscript/codegen/PartialApps.mls index 92c6963035..f2d75f6cde 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/PartialApps.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/PartialApps.mls @@ -227,18 +227,14 @@ _ - _ of 1, 2 :sjs 1 \ (_ - 2) //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let lambda32; -//β”‚ lambda32 = (undefined, function (_0) { return _0 - 2 }); -//β”‚ return Predef.passTo(1, lambda32) +//β”‚ let lambda32; lambda32 = (undefined, function (_0) { return _0 - 2 }); Predef.passTo(1, lambda32); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = fun :sjs 1 \ (_ - 2)() //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let lambda33; -//β”‚ lambda33 = (undefined, function (_0) { return _0 - 2 }); -//β”‚ return Predef.passTo(1, lambda33)() +//β”‚ let lambda33; lambda33 = (undefined, function (_0) { return _0 - 2 }); Predef.passTo(1, lambda33)(); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = -1 @@ -251,10 +247,10 @@ _ - _ of 1, 2 :w let f = if _ then 1 else 0 //β”‚ ╔══[WARNING] This catch-all clause makes the following branches unreachable. -//β”‚ β•‘ l.252: let f = if _ then 1 else 0 +//β”‚ β•‘ l.248: let f = if _ then 1 else 0 //β”‚ β•‘ ^^^^^^ //β”‚ β•Ÿβ”€β”€ This branch is unreachable. -//β”‚ β•‘ l.252: let f = if _ then 1 else 0 +//β”‚ β•‘ l.248: let f = if _ then 1 else 0 //β”‚ ╙── ^^^^^^ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ let f13; f13 = 1; diff --git a/hkmc2/shared/src/test/mlscript/codegen/PlainClasses.mls b/hkmc2/shared/src/test/mlscript/codegen/PlainClasses.mls index 9cc5bf21ba..e1e5d217a1 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/PlainClasses.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/PlainClasses.mls @@ -18,41 +18,38 @@ class Foo Foo is Foo //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ if (Foo1 instanceof Foo1) { return true } return false; +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = false (new Foo) is Foo //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let scrut; -//β”‚ scrut = globalThis.Object.freeze(new Foo1()); -//β”‚ if (scrut instanceof Foo1) { return true } -//β”‚ return false; +//β”‚ let scrut; scrut = globalThis.Object.freeze(new Foo1()); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = true new Foo //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return globalThis.Object.freeze(new Foo1()) +//β”‚ globalThis.Object.freeze(new Foo1()); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = Foo new Foo() //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return globalThis.Object.freeze(new Foo1()) +//β”‚ globalThis.Object.freeze(new Foo1()); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = Foo Foo //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return Foo1 +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = class Foo :re Foo() //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return runtime.safeCall(Foo1()) +//β”‚ runtime.safeCall(Foo1()); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ ═══[RUNTIME ERROR] TypeError: Class constructor Foo cannot be invoked without 'new' @@ -71,7 +68,7 @@ print("ok") //β”‚ toString() { return runtime.render(this); } //β”‚ static [definitionMetadata] = ["class", "Foo"]; //β”‚ }); -//β”‚ return Predef.print("ok") +//β”‚ Predef.print("ok"); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ > ok @@ -120,18 +117,18 @@ let t = test() :e new t //β”‚ ╔══[COMPILATION ERROR] Expected a statically known class; found reference. -//β”‚ β•‘ l.121: new t +//β”‚ β•‘ l.118: new t //β”‚ β•‘ ^ //β”‚ ╙── The 'new' keyword requires a statically known class; use the 'new!' operator for dynamic instantiation. //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return globalThis.Object.freeze(new t()) +//β”‚ globalThis.Object.freeze(new t()); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ > hi //β”‚ = Foo new! t //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return globalThis.Object.freeze(new t()) +//β”‚ globalThis.Object.freeze(new t()); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ > hi //β”‚ = Foo @@ -139,18 +136,18 @@ new! t :e new t() //β”‚ ╔══[COMPILATION ERROR] Expected a statically known class; found reference. -//β”‚ β•‘ l.140: new t() +//β”‚ β•‘ l.137: new t() //β”‚ β•‘ ^ //β”‚ ╙── The 'new' keyword requires a statically known class; use the 'new!' operator for dynamic instantiation. //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return globalThis.Object.freeze(new t()) +//β”‚ globalThis.Object.freeze(new t()); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ > hi //β”‚ = Foo new! t() //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return globalThis.Object.freeze(new t()) +//β”‚ globalThis.Object.freeze(new t()); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ > hi //β”‚ = Foo @@ -248,7 +245,7 @@ print(a.bar(1)) //β”‚ tmp = runtime.safeCall(a.foo(1)); //β”‚ Predef.print(tmp); //β”‚ tmp1 = runtime.safeCall(a.bar(1)); -//β”‚ return Predef.print(tmp1) +//β”‚ Predef.print(tmp1); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ > 1 //β”‚ > 2 @@ -263,10 +260,10 @@ class Foo with val x = 2 //β”‚ ╔══[COMPILATION ERROR] Multiple definitions of symbol 'x' //β”‚ β•Ÿβ”€β”€ defined here -//β”‚ β•‘ l.262: val x = 1 +//β”‚ β•‘ l.259: val x = 1 //β”‚ β•‘ ^^^^^^^^^ //β”‚ β•Ÿβ”€β”€ defined here -//β”‚ β•‘ l.263: val x = 2 +//β”‚ β•‘ l.260: val x = 2 //β”‚ ╙── ^^^^^^^^^ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ let Foo14; @@ -288,10 +285,10 @@ class Foo with val x = 1 let x = 2 //β”‚ ╔══[COMPILATION ERROR] Name 'x' is already used -//β”‚ β•‘ l.289: let x = 2 +//β”‚ β•‘ l.286: let x = 2 //β”‚ β•‘ ^^^^^ //β”‚ β•Ÿβ”€β”€ by a member declared in the same block -//β”‚ β•‘ l.288: val x = 1 +//β”‚ β•‘ l.285: val x = 1 //β”‚ ╙── ^^^^^^^^^ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ let Foo16; @@ -317,7 +314,7 @@ class Foo with :e class Foo with val x = 1 //β”‚ ╔══[COMPILATION ERROR] Illegal body of class definition (should be a block; found term definition). -//β”‚ β•‘ l.318: class Foo with val x = 1 +//β”‚ β•‘ l.315: class Foo with val x = 1 //β”‚ ╙── ^^^^^^^^^ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ let Foo18; diff --git a/hkmc2/shared/src/test/mlscript/codegen/PredefUsage.mls b/hkmc2/shared/src/test/mlscript/codegen/PredefUsage.mls index d1c04598de..20e617b5f2 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/PredefUsage.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/PredefUsage.mls @@ -18,7 +18,7 @@ print(12) :sjs 12 |> print //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return Predef.pipeInto(12, Predef.print) +//β”‚ Predef.pipeInto(12, Predef.print); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ > 12 diff --git a/hkmc2/shared/src/test/mlscript/codegen/Pwd.mls b/hkmc2/shared/src/test/mlscript/codegen/Pwd.mls index 74db669543..a51a462364 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/Pwd.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/Pwd.mls @@ -13,8 +13,6 @@ in folderName2 === folderName1 || folderName2 === "shared" //β”‚ tmp2 = runtime.safeCall(tmp1.split("/")); //β”‚ folderName2 = runtime.safeCall(tmp2.pop()); //β”‚ tmp3 = folderName2 === folderName1; -//β”‚ if (tmp3 === false) { return folderName2 === "shared" } -//β”‚ return true; //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = true diff --git a/hkmc2/shared/src/test/mlscript/codegen/QQImport.mls b/hkmc2/shared/src/test/mlscript/codegen/QQImport.mls index 3bbaa93b76..f1b125027e 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/QQImport.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/QQImport.mls @@ -1,7 +1,12 @@ :js :qq +:silent +import "../../mlscript-compile/Term.mls" +module meta with + fun codegen(t, file) = Term.codegen(t, file) + fun print(t) = Term.print(t) meta.print //β”‚ = fun print diff --git a/hkmc2/shared/src/test/mlscript/codegen/Quasiquotes.mls b/hkmc2/shared/src/test/mlscript/codegen/Quasiquotes.mls index 73a6287fb9..3de9b26521 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/Quasiquotes.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/Quasiquotes.mls @@ -40,7 +40,7 @@ x `=> //β”‚ x1 = globalThis.Object.freeze(new Term.Ref(tmp5)); //β”‚ Predef.print(x1); //β”‚ arr2 = globalThis.Object.freeze([ tmp5 ]); -//β”‚ return globalThis.Object.freeze(new Term.Lam(arr2, x1)) +//β”‚ globalThis.Object.freeze(new Term.Lam(arr2, x1)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ > Ref(Symbol("x")) //β”‚ = Lam([Symbol("x")], Ref(Symbol("x"))) @@ -87,7 +87,7 @@ f`(`0) //β”‚ tmp28 = globalThis.Object.freeze(new Term.Else(tmp29)); //β”‚ tmp23 = globalThis.Object.freeze(new Term.Cons(tmp27, tmp28)); //β”‚ tmp24 = globalThis.Object.freeze(new Term.Let(tmp21, tmp22, tmp23)); -//β”‚ return globalThis.Object.freeze(new Term.IfLike(Term.Keyword.If, tmp24)) +//β”‚ globalThis.Object.freeze(new Term.IfLike(Term.Keyword.If, tmp24)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = IfLike( //β”‚ If, diff --git a/hkmc2/shared/src/test/mlscript/codegen/RandomStuff.mls b/hkmc2/shared/src/test/mlscript/codegen/RandomStuff.mls index a48cbe1e6b..6d624a6d0f 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/RandomStuff.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/RandomStuff.mls @@ -80,7 +80,6 @@ do //β”‚ loopLabel: while (true) { continue loopLabel; } //β”‚ }); //β”‚ Predef.print(lambda); -//β”‚ return runtime.Unit //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ > fun //β”‚ f = 1 @@ -90,10 +89,10 @@ do let foo = 1 fun foo(x) = foo //β”‚ ╔══[COMPILATION ERROR] Name 'foo' is already used -//β”‚ β•‘ l.90: let foo = 1 +//β”‚ β•‘ l.89: let foo = 1 //β”‚ β•‘ ^^^^^^^ //β”‚ β•Ÿβ”€β”€ by a member declared in the same block -//β”‚ β•‘ l.91: fun foo(x) = foo +//β”‚ β•‘ l.90: fun foo(x) = foo //β”‚ ╙── ^^^^^^^^^^^^^^^^ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ let foo4; foo4 = 1; @@ -104,7 +103,7 @@ fun foo(x) = foo :re foo(1) //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return runtime.safeCall(foo4(1)) +//β”‚ runtime.safeCall(foo4(1)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ ═══[RUNTIME ERROR] TypeError: foo4 is not a function diff --git a/hkmc2/shared/src/test/mlscript/codegen/SetIn.mls b/hkmc2/shared/src/test/mlscript/codegen/SetIn.mls index 3b8ed23c41..ce2439ad80 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/SetIn.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/SetIn.mls @@ -17,13 +17,13 @@ x :sjs set x = 0 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ x1 = 0; return runtime.Unit +//β”‚ x1 = 0; //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” :sjs set x += 1 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let tmp; tmp = x1 + 1; x1 = tmp; return runtime.Unit +//β”‚ let tmp; tmp = x1 + 1; x1 = tmp; //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” x @@ -35,13 +35,8 @@ set x += 1 in print(x) //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ let old, tmp1, tmp2, tmp3; //β”‚ old = x1; -//β”‚ try { -//β”‚ tmp2 = x1 + 1; -//β”‚ x1 = tmp2; -//β”‚ tmp3 = Predef.print(tmp2); -//β”‚ tmp1 = tmp3; -//β”‚ } finally { x1 = old; } -//β”‚ return tmp1 +//β”‚ try { tmp2 = x1 + 1; x1 = tmp2; tmp3 = Predef.print(tmp2); tmp1 = tmp3; } finally { x1 = old; } +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ > 2 @@ -103,7 +98,7 @@ example() //β”‚ tmp5 = runtime.safeCall(get_x()); //β”‚ return Predef.print(tmp5) //β”‚ }; -//β”‚ return example2() +//β”‚ example2(); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ > 1 //β”‚ > 1 diff --git a/hkmc2/shared/src/test/mlscript/codegen/Spreads.mls b/hkmc2/shared/src/test/mlscript/codegen/Spreads.mls index 9546efd078..f16879f31e 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/Spreads.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/Spreads.mls @@ -19,7 +19,7 @@ foo(0, ...a) :sjs foo(1, ...[2, 3], 4) //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let tmp; tmp = globalThis.Object.freeze([ 2, 3 ]); return foo(1, ...tmp, 4) +//β”‚ let tmp; tmp = globalThis.Object.freeze([ 2, 3 ]); foo(1, ...tmp, 4); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = [1, 2, 3, 4] diff --git a/hkmc2/shared/src/test/mlscript/codegen/ThisCallVariations.mls b/hkmc2/shared/src/test/mlscript/codegen/ThisCallVariations.mls index 51a40a4d6c..80427dfb06 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/ThisCallVariations.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/ThisCallVariations.mls @@ -39,7 +39,7 @@ Example .!. oops(2) //β”‚ rhs = Tup of Ls of //β”‚ IntLit of 2 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return runtime.safeCall(oops.call(Example1, 2)) +//β”‚ runtime.safeCall(oops.call(Example1, 2)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = [2, 1] @@ -105,7 +105,7 @@ Example .> oops(2) //β”‚ rhs = Tup of Ls of //β”‚ IntLit of 2 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let tmp7; tmp7 = runtime.safeCall(oops(2)); return call1(Example1, tmp7) +//β”‚ let tmp7; tmp7 = runtime.safeCall(oops(2)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ ═══[RUNTIME ERROR] TypeError: Cannot read properties of undefined (reading 'a') diff --git a/hkmc2/shared/src/test/mlscript/codegen/ThisCalls.mls b/hkmc2/shared/src/test/mlscript/codegen/ThisCalls.mls index acebbd4e5b..927b7626ec 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/ThisCalls.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/ThisCalls.mls @@ -19,7 +19,7 @@ s(123) :sjs ex |>. s(123) //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return Predef.call(ex, s)(123) +//β”‚ Predef.call(ex, s)(123); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = [123, 456] diff --git a/hkmc2/shared/src/test/mlscript/codegen/UnitValue.mls b/hkmc2/shared/src/test/mlscript/codegen/UnitValue.mls index b9c3ef6a79..7a7d16c739 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/UnitValue.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/UnitValue.mls @@ -38,7 +38,7 @@ print of foo() :sjs print of globalThis.console.log("Hello, world!") //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let tmp; tmp = runtime.safeCall(globalThis.console.log("Hello, world!")); return Predef.print(tmp) +//β”‚ let tmp; tmp = runtime.safeCall(globalThis.console.log("Hello, world!")); Predef.print(tmp); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ > Hello, world! //β”‚ > () diff --git a/hkmc2/shared/src/test/mlscript/codegen/While.mls b/hkmc2/shared/src/test/mlscript/codegen/While.mls index 5294b2c49f..6ddc497a2b 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/While.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/While.mls @@ -70,7 +70,6 @@ f //β”‚ break inlinedLbl; //β”‚ } //β”‚ } -//β”‚ return inlinedVal //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ > Hello World //β”‚ = 42 @@ -251,11 +250,7 @@ let x = 1 :sjs while x is {} do() //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ lbl11: while (true) { -//β”‚ if (x3 instanceof globalThis.Object) { continue lbl11 } -//β”‚ break; -//β”‚ } -//β”‚ return runtime.Unit +//β”‚ lbl11: while (true) { if (x3 instanceof globalThis.Object) { continue lbl11 } break; } //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” @@ -327,10 +322,10 @@ while print("Hello World"); false then 0(0) else 1 //β”‚ ╔══[PARSE ERROR] Unexpected 'then' keyword here -//β”‚ β•‘ l.327: then 0(0) +//β”‚ β•‘ l.322: then 0(0) //β”‚ ╙── ^^^^ //β”‚ ╔══[COMPILATION ERROR] Unrecognized term split (false literal) -//β”‚ β•‘ l.326: while print("Hello World"); false +//β”‚ β•‘ l.321: while print("Hello World"); false //β”‚ ╙── ^^^^^ //β”‚ > Hello World @@ -339,12 +334,12 @@ while { print("Hello World"), false } then 0(0) else 1 //β”‚ ╔══[COMPILATION ERROR] Unexpected infix use of keyword 'then' here -//β”‚ β•‘ l.338: while { print("Hello World"), false } +//β”‚ β•‘ l.333: while { print("Hello World"), false } //β”‚ β•‘ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -//β”‚ β•‘ l.339: then 0(0) +//β”‚ β•‘ l.334: then 0(0) //β”‚ ╙── ^^^^^^^^^^^ //β”‚ ╔══[COMPILATION ERROR] Illegal position for prefix keyword 'else'. -//β”‚ β•‘ l.340: else 1 +//β”‚ β•‘ l.335: else 1 //β”‚ ╙── ^^^^ //β”‚ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. @@ -355,14 +350,14 @@ while then 0(0) else 1 //β”‚ ╔══[COMPILATION ERROR] Unexpected infix use of keyword 'then' here -//β”‚ β•‘ l.353: print("Hello World") +//β”‚ β•‘ l.348: print("Hello World") //β”‚ β•‘ ^^^^^^^^^^^^^^^^^^^^ -//β”‚ β•‘ l.354: false +//β”‚ β•‘ l.349: false //β”‚ β•‘ ^^^^^^^^^ -//β”‚ β•‘ l.355: then 0(0) +//β”‚ β•‘ l.350: then 0(0) //β”‚ ╙── ^^^^^^^^^^^ //β”‚ ╔══[COMPILATION ERROR] Illegal position for prefix keyword 'else'. -//β”‚ β•‘ l.356: else 1 +//β”‚ β•‘ l.351: else 1 //β”‚ ╙── ^^^^ //β”‚ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. diff --git a/hkmc2/shared/src/test/mlscript/codegen/WhileDefaults.mls b/hkmc2/shared/src/test/mlscript/codegen/WhileDefaults.mls index 9231f27125..b527cc37af 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/WhileDefaults.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/WhileDefaults.mls @@ -11,15 +11,11 @@ //β”‚ let lambda; //β”‚ lambda = (undefined, function () { //β”‚ lbl: while (true) { -//β”‚ if (false === true) { -//β”‚ Predef.print(1); -//β”‚ continue lbl -//β”‚ } +//β”‚ if (false === true) { Predef.print(1); continue lbl } //β”‚ Predef.print(2); //β”‚ continue lbl; //β”‚ } //β”‚ }); -//β”‚ return lambda //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = fun diff --git a/hkmc2/shared/src/test/mlscript/ctx/ClassCtxParams.mls b/hkmc2/shared/src/test/mlscript/ctx/ClassCtxParams.mls index cb6e047e12..5641d282da 100644 --- a/hkmc2/shared/src/test/mlscript/ctx/ClassCtxParams.mls +++ b/hkmc2/shared/src/test/mlscript/ctx/ClassCtxParams.mls @@ -131,7 +131,7 @@ x => x.Foo#S //β”‚ β•‘ ^ //β”‚ ╙── Missing instance: Expected: T; Available: Int //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let lambda; lambda = (undefined, function (x) { return x.S }); return lambda +//β”‚ let lambda; lambda = (undefined, function (x) { return x.S }); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = fun diff --git a/hkmc2/shared/src/test/mlscript/ctx/MissingDefinitions2.mls b/hkmc2/shared/src/test/mlscript/ctx/MissingDefinitions2.mls index d53861141c..9a40599c6b 100644 --- a/hkmc2/shared/src/test/mlscript/ctx/MissingDefinitions2.mls +++ b/hkmc2/shared/src/test/mlscript/ctx/MissingDefinitions2.mls @@ -49,7 +49,7 @@ test(1, 2) :re 1 ++ 1 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let tmp1; tmp1 = test(); return runtime.safeCall(tmp1(1, 1)) +//β”‚ let tmp1; tmp1 = test(); runtime.safeCall(tmp1(1, 1)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ ═══[RUNTIME ERROR] TypeError: test is not a function diff --git a/hkmc2/shared/src/test/mlscript/decls/Prelude.mls b/hkmc2/shared/src/test/mlscript/decls/Prelude.mls index ea29c8c24b..7839cef760 100644 --- a/hkmc2/shared/src/test/mlscript/decls/Prelude.mls +++ b/hkmc2/shared/src/test/mlscript/decls/Prelude.mls @@ -10,6 +10,7 @@ declare module Object with create freeze assign + keys entries prototype fromEntries @@ -20,6 +21,7 @@ declare module Object with declare class JSON declare module JSON with fun + parse stringify declare class Number declare module Number with @@ -30,6 +32,8 @@ declare module Number with MAX_SAFE_INTEGER NEGATIVE_INFINITY POSITIVE_INFINITY + fun + isFinite declare class BigInt declare module BigInt declare class Function @@ -54,7 +58,9 @@ declare class Error//(info) // TODO: handle JS classes that can be instantiated declare class TypeError//(info) // TODO: handle JS classes that can be instantiated without `new` specially in codegen. declare class RangeError//(info) // TODO: handle JS classes that can be instantiated without `new` specially in codegen. declare class Date -declare module Date +declare module Date with + fun + now declare class ArrayBuffer declare module ArrayBuffer @@ -184,6 +190,7 @@ declare module Reflect with getPrototypeOf apply construct + deleteProperty declare module console with declare fun @@ -288,5 +295,11 @@ declare module runtime with // HTML DOM API definitions. // Move them to a separate Prelude file when there are enough of them. declare val document +declare val window +declare val localStorage +declare val self declare val customElements declare class HTMLElement +declare class CustomEvent +declare class Worker +declare val Intl diff --git a/hkmc2/shared/src/test/mlscript/deforest/fusibility.mls b/hkmc2/shared/src/test/mlscript/deforest/fusibility.mls index 6b71eac0f2..d7bd587d5a 100644 --- a/hkmc2/shared/src/test/mlscript/deforest/fusibility.mls +++ b/hkmc2/shared/src/test/mlscript/deforest/fusibility.mls @@ -154,7 +154,7 @@ c(AA(1)) //β”‚ deforest > <<< fusing <<< //β”‚ = 0 - +// TODO: The variable numbering in flow analysis is not stable. // non-affinity propagates through nested ctors fun c1(x) = if x is AA(AA(AA(v))) then v else 0 fun c2(x) = if x is AA(AA(_)) then 1 else 0 diff --git a/hkmc2/shared/src/test/mlscript/flows/SelExpansion.mls b/hkmc2/shared/src/test/mlscript/flows/SelExpansion.mls index 272c495ca6..d7333cf281 100644 --- a/hkmc2/shared/src/test/mlscript/flows/SelExpansion.mls +++ b/hkmc2/shared/src/test/mlscript/flows/SelExpansion.mls @@ -64,7 +64,7 @@ f.foo //β”‚ f⁰ <~ Foo() //β”‚ f⁰ ~> {a: β‹…a⁰} {a: β‹…aΒΉ} {foo: β‹…foo⁰} //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return runtime.safeCall(Foo1.foo(f)) +//β”‚ runtime.safeCall(Foo1.foo(f)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 123 @@ -219,7 +219,7 @@ new AA.BB.CC().x //β”‚ where //β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let tmp1; tmp1 = globalThis.Object.freeze(new AA1.BB.CC()); return tmp1.x +//β”‚ let tmp1; tmp1 = globalThis.Object.freeze(new AA1.BB.CC()); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 1 @@ -230,9 +230,7 @@ new AA.BB.CC().getX //β”‚ where //β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let tmp2; -//β”‚ tmp2 = globalThis.Object.freeze(new AA1.BB.CC()); -//β”‚ return runtime.safeCall(AA1.BB.CC.getX(tmp2)) +//β”‚ let tmp2; tmp2 = globalThis.Object.freeze(new AA1.BB.CC()); runtime.safeCall(AA1.BB.CC.getX(tmp2)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 1 @@ -263,7 +261,7 @@ let foo = :re foo.test //β”‚ ╔══[COMPILATION ERROR] Cannot access companion A from the context of this selection -//β”‚ β•‘ l.264: foo.test +//β”‚ β•‘ l.262: foo.test //β”‚ ╙── ^^^^^^^^ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Flowed |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ fooΒΉ.testβ€Ή?β€Ί @@ -302,7 +300,7 @@ let foo = :re foo.test //β”‚ ╔══[COMPILATION ERROR] Cannot access companion A from the context of this selection -//β”‚ β•‘ l.303: foo.test +//β”‚ β•‘ l.301: foo.test //β”‚ ╙── ^^^^^^^^ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Flowed |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ fooΒ².testβ€Ή?β€Ί diff --git a/hkmc2/shared/src/test/mlscript/handlers/Effects.mls b/hkmc2/shared/src/test/mlscript/handlers/Effects.mls index 0d4985ba25..9c6f937214 100644 --- a/hkmc2/shared/src/test/mlscript/handlers/Effects.mls +++ b/hkmc2/shared/src/test/mlscript/handlers/Effects.mls @@ -179,9 +179,7 @@ result //β”‚ } //β”‚ }); //β”‚ runtime.enterHandleBlock(h7, handleBlock$7); -//β”‚ if (runtime.curEffect === null) { return result } -//β”‚ runtime.topLevelEffect(false); -//β”‚ return result; +//β”‚ if (runtime.curEffect !== null) { runtime.topLevelEffect(false); } //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = "54321" //β”‚ result = "54321" @@ -481,28 +479,28 @@ handle h = Eff with fun perform()(k) = k(()) foo(h) //β”‚ ╔══[WARNING] Modules are not yet lifted. -//β”‚ β•‘ l.476: module A with +//β”‚ β•‘ l.474: module A with //β”‚ ╙── ^ //β”‚ ╔══[WARNING] Modules are not yet lifted. -//β”‚ β•‘ l.472: module A with +//β”‚ β•‘ l.470: module A with //β”‚ ╙── ^ //β”‚ ╔══[WARNING] Modules are not yet lifted. -//β”‚ β•‘ l.468: module A with +//β”‚ β•‘ l.466: module A with //β”‚ ╙── ^ //β”‚ ╔══[WARNING] Modules are not yet lifted. -//β”‚ β•‘ l.464: module A with +//β”‚ β•‘ l.462: module A with //β”‚ ╙── ^ //β”‚ ╔══[INTERNAL ERROR] Unexpected nested class: lambdas may not function correctly. -//β”‚ β•‘ l.464: module A with +//β”‚ β•‘ l.462: module A with //β”‚ ╙── ^ //β”‚ ╔══[INTERNAL ERROR] Unexpected nested class: lambdas may not function correctly. -//β”‚ β•‘ l.468: module A with +//β”‚ β•‘ l.466: module A with //β”‚ ╙── ^ //β”‚ ╔══[INTERNAL ERROR] Unexpected nested class: lambdas may not function correctly. -//β”‚ β•‘ l.472: module A with +//β”‚ β•‘ l.470: module A with //β”‚ ╙── ^ //β”‚ ╔══[INTERNAL ERROR] Unexpected nested class: lambdas may not function correctly. -//β”‚ β•‘ l.476: module A with +//β”‚ β•‘ l.474: module A with //β”‚ ╙── ^ //β”‚ = 123 diff --git a/hkmc2/shared/src/test/mlscript/handlers/NoStackSafety.mls b/hkmc2/shared/src/test/mlscript/handlers/NoStackSafety.mls index e760f793c2..4a23e7aaca 100644 --- a/hkmc2/shared/src/test/mlscript/handlers/NoStackSafety.mls +++ b/hkmc2/shared/src/test/mlscript/handlers/NoStackSafety.mls @@ -37,7 +37,7 @@ fun hi(n) = n hi(0) //β”‚ /!!!\ Option ':stackSafe' requires ':effectHandlers' to be set //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let hi; hi = function hi(n) { return n }; return 0 +//β”‚ let hi; hi = function hi(n) { return n }; //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 0 diff --git a/hkmc2/shared/src/test/mlscript/handlers/RecursiveHandlers.mls b/hkmc2/shared/src/test/mlscript/handlers/RecursiveHandlers.mls index 91a5afb63c..6452dc6e91 100644 --- a/hkmc2/shared/src/test/mlscript/handlers/RecursiveHandlers.mls +++ b/hkmc2/shared/src/test/mlscript/handlers/RecursiveHandlers.mls @@ -315,13 +315,8 @@ str //β”‚ if (true === true) { //β”‚ h11 = new Handler$h1$3(); //β”‚ runtime.enterHandleBlock(h11, handleBlock$10); -//β”‚ if (runtime.curEffect === null) { -//β”‚ return str -//β”‚ } -//β”‚ runtime.topLevelEffect(false); -//β”‚ return str; +//β”‚ if (runtime.curEffect !== null) { runtime.topLevelEffect(false); } //β”‚ } -//β”‚ return str; //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = "BABABA" //β”‚ str = "BABABA" diff --git a/hkmc2/shared/src/test/mlscript/handlers/SetInHandlers.mls b/hkmc2/shared/src/test/mlscript/handlers/SetInHandlers.mls index 26bb005dc1..1cb71bcc13 100644 --- a/hkmc2/shared/src/test/mlscript/handlers/SetInHandlers.mls +++ b/hkmc2/shared/src/test/mlscript/handlers/SetInHandlers.mls @@ -7,7 +7,7 @@ let x = 1 set x += 1 in print(x) x //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let x, old; x = 1; old = 1; try { x = 2; Predef.print(2); } finally { x = old; } return old +//β”‚ let x, old; x = 1; old = 1; try { x = 2; Predef.print(2); } finally { x = old; } //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ > 2 //β”‚ = 1 diff --git a/hkmc2/shared/src/test/mlscript/handlers/StackSafety.mls b/hkmc2/shared/src/test/mlscript/handlers/StackSafety.mls index b9dd69a80f..79dae47157 100644 --- a/hkmc2/shared/src/test/mlscript/handlers/StackSafety.mls +++ b/hkmc2/shared/src/test/mlscript/handlers/StackSafety.mls @@ -35,8 +35,7 @@ hi(0) //β”‚ return hi(0) //β”‚ }); //β”‚ tmp = runtime.runStackSafe(6, $_stack$_safe$_body$_); -//β”‚ if (runtime.curEffect === null) { return tmp } -//β”‚ return runtime.topLevelEffect(false); +//β”‚ if (runtime.curEffect !== null) { runtime.topLevelEffect(false); } //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 0 @@ -74,22 +73,21 @@ sum(10000) //β”‚ if (runtime.curEffect === null) { //β”‚ pc = 1; //β”‚ } else { -//β”‚ return runtime.unwind(sum, 1, "StackSafety.mls:49:9", null, null, 1, 1, n, 0) +//β”‚ return runtime.unwind(sum, 1, "StackSafety.mls:48:9", null, null, 1, 1, n, 0) //β”‚ } //β”‚ case 1: //β”‚ tmp3 = runtime.resumeValue; //β”‚ return n + tmp3; //β”‚ } //β”‚ } else { -//β”‚ return runtime.unwind(sum, pc, "StackSafety.mls:46:1", null, null, 1, 1, n, 0) +//β”‚ return runtime.unwind(sum, pc, "StackSafety.mls:45:1", null, null, 1, 1, n, 0) //β”‚ } //β”‚ }; //β”‚ $_stack$_safe$_body$_1 = (undefined, function () { //β”‚ return sum(10000) //β”‚ }); //β”‚ tmp1 = runtime.runStackSafe(1000, $_stack$_safe$_body$_1); -//β”‚ if (runtime.curEffect === null) { return tmp1 } -//β”‚ return runtime.topLevelEffect(false); +//β”‚ if (runtime.curEffect !== null) { runtime.topLevelEffect(false); } //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 50005000 diff --git a/hkmc2/shared/src/test/mlscript/handlers/TailCallOptimization.mls b/hkmc2/shared/src/test/mlscript/handlers/TailCallOptimization.mls index 295e580f5d..eb7820c2ac 100644 --- a/hkmc2/shared/src/test/mlscript/handlers/TailCallOptimization.mls +++ b/hkmc2/shared/src/test/mlscript/handlers/TailCallOptimization.mls @@ -57,7 +57,6 @@ hi(0) //β”‚ return hi(0) //β”‚ }); //β”‚ tmp1 = runtime.runStackSafe(1000, $_stack$_safe$_body$_); -//β”‚ if (runtime.curEffect === null) { return tmp1 } -//β”‚ return runtime.topLevelEffect(false); +//β”‚ if (runtime.curEffect !== null) { runtime.topLevelEffect(false); } //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 0 diff --git a/hkmc2/shared/src/test/mlscript/interop/Arrays.mls b/hkmc2/shared/src/test/mlscript/interop/Arrays.mls index 372a72581b..b179d89912 100644 --- a/hkmc2/shared/src/test/mlscript/interop/Arrays.mls +++ b/hkmc2/shared/src/test/mlscript/interop/Arrays.mls @@ -17,7 +17,7 @@ arr.map((e, ...) => e === false) //β”‚ lambda1 = (undefined, function (e, ..._) { //β”‚ return e === false //β”‚ }); -//β”‚ return runtime.safeCall(arr.map(lambda1)) +//β”‚ runtime.safeCall(arr.map(lambda1)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = [false, true] @@ -26,7 +26,7 @@ arr.map((_, ...) => 1) //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ let lambda2; //β”‚ lambda2 = (undefined, function (_, ..._1) { return 1 }); -//β”‚ return runtime.safeCall(arr.map(lambda2)) +//β”‚ runtime.safeCall(arr.map(lambda2)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = [1, 1] diff --git a/hkmc2/shared/src/test/mlscript/interop/CtorBypass.mls b/hkmc2/shared/src/test/mlscript/interop/CtorBypass.mls index c3756f7c68..d20ab62c49 100644 --- a/hkmc2/shared/src/test/mlscript/interop/CtorBypass.mls +++ b/hkmc2/shared/src/test/mlscript/interop/CtorBypass.mls @@ -23,7 +23,7 @@ let b = Object.create(A.prototype) :sjs b is A //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ if (b instanceof A1) { return true } return false; +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = true diff --git a/hkmc2/shared/src/test/mlscript/invalml/InvalMLCodeGen.mls b/hkmc2/shared/src/test/mlscript/invalml/InvalMLCodeGen.mls index f5a3a22735..e87deb28eb 100644 --- a/hkmc2/shared/src/test/mlscript/invalml/InvalMLCodeGen.mls +++ b/hkmc2/shared/src/test/mlscript/invalml/InvalMLCodeGen.mls @@ -7,7 +7,7 @@ :sjs 42 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return 42 +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 42 //β”‚ Type: Int @@ -15,7 +15,7 @@ :sjs 1 + 1 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return 1 + 1 +//β”‚ 1 + 1; //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 2 //β”‚ Type: Int @@ -24,7 +24,7 @@ :sjs 1.0 +. 2.14 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return 1.0 + 2.14 +//β”‚ 1.0 + 2.14; //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 3.14 //β”‚ Type: Num @@ -33,7 +33,7 @@ :sjs let x = 1 in x + 1 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return 1 + 1 +//β”‚ 1 + 1; //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 2 //β”‚ Type: Int @@ -42,7 +42,7 @@ let x = 1 in x + 1 :sjs "abc" //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return "abc" +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = "abc" //β”‚ Type: Str @@ -50,7 +50,7 @@ let x = 1 in x + 1 :sjs false //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return false +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = false //β”‚ Type: Bool @@ -58,7 +58,7 @@ false :sjs (x => x) as [T] -> T -> T //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let lambda1; lambda1 = (undefined, function (x) { return x }); return lambda1 +//β”‚ let lambda1; lambda1 = (undefined, function (x) { return x }); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = fun //β”‚ Type: ['T] -> ('T) ->{βŠ₯} 'T @@ -87,7 +87,7 @@ data class Foo(x: Int) :sjs new Foo(42) //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return globalThis.Object.freeze(new Foo1.class(42)) +//β”‚ globalThis.Object.freeze(new Foo1.class(42)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = Foo(42) //β”‚ Type: Foo @@ -96,7 +96,7 @@ new Foo(42) :sjs let foo = new Foo(42) in foo.Foo#x //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let foo; foo = globalThis.Object.freeze(new Foo1.class(42)); return foo.x +//β”‚ let foo; foo = globalThis.Object.freeze(new Foo1.class(42)); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 42 //β”‚ Type: Int @@ -112,7 +112,7 @@ fun inc(x) = x + 1 :sjs inc(41) //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return 41 + 1 +//β”‚ 41 + 1; //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 42 //β”‚ Type: Int @@ -121,7 +121,7 @@ inc(41) :sjs if 1 == 2 then 0 else 42 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let scrut; scrut = 1 == 2; if (scrut === true) { return 0 } return 42; +//β”‚ let scrut; scrut = 1 == 2; //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 42 //β”‚ Type: Int @@ -130,7 +130,7 @@ if 1 == 2 then 0 else 42 :sjs if 1 is Int then 1 else 0 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ if (globalThis.Number.isInteger(1)) { return 1 } return 0; +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 1 //β”‚ Type: Int @@ -144,10 +144,7 @@ data class Foo() let foo = new Foo() if foo is Foo then 1 else 0 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let foo1; -//β”‚ foo1 = globalThis.Object.freeze(new Foo3.class()); -//β”‚ if (foo1 instanceof Foo3.class) { return 1 } -//β”‚ return 0; +//β”‚ let foo1; foo1 = globalThis.Object.freeze(new Foo3.class()); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 1 //β”‚ foo = Foo() @@ -203,12 +200,9 @@ fun nott = case :sjs nott of false //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ if (false === true) { -//β”‚ return false -//β”‚ } else if (false === false) { -//β”‚ return true +//β”‚ if (false === true) {} else if (false === false) {} else { +//β”‚ throw globalThis.Object.freeze(new globalThis.Error.class("match error")) //β”‚ } -//β”‚ throw globalThis.Object.freeze(new globalThis.Error.class("match error")); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = true //β”‚ Type: Bool @@ -246,7 +240,7 @@ fact(3) :sjs region x in 42 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ new globalThis.Region(); return 42 +//β”‚ new globalThis.Region(); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 42 //β”‚ Type: Int @@ -255,7 +249,7 @@ region x in 42 :sjs region x in x //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return new globalThis.Region() +//β”‚ new globalThis.Region(); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = Region //β”‚ Type: Region[?] @@ -264,7 +258,7 @@ region x in x :sjs region x in x.ref 42 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let x; x = new globalThis.Region(); return new globalThis.Ref(x, 42) +//β”‚ let x; x = new globalThis.Region(); new globalThis.Ref(x, 42); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = Ref(Region, 42) //β”‚ Type: Ref[Int, ?] @@ -273,7 +267,7 @@ region x in x.ref 42 :sjs region x in let y = x.ref 42 in !(y) //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let x1, y; x1 = new globalThis.Region(); y = new globalThis.Ref(x1, 42); return y.value +//β”‚ let x1, y; x1 = new globalThis.Region(); y = new globalThis.Ref(x1, 42); y.value; //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 42 //β”‚ Type: Int @@ -282,7 +276,7 @@ region x in let y = x.ref 42 in !(y) :sjs region x in let y = x.ref 42 in y := 0 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let x2, y1; x2 = new globalThis.Region(); y1 = new globalThis.Ref(x2, 42); y1.value = 0; return 0 +//β”‚ let x2, y1; x2 = new globalThis.Region(); y1 = new globalThis.Ref(x2, 42); y1.value = 0; //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 0 //β”‚ Type: Int diff --git a/hkmc2/shared/src/test/mlscript/invalml/InvalMLGetters.mls b/hkmc2/shared/src/test/mlscript/invalml/InvalMLGetters.mls index ebff000249..8e80510379 100644 --- a/hkmc2/shared/src/test/mlscript/invalml/InvalMLGetters.mls +++ b/hkmc2/shared/src/test/mlscript/invalml/InvalMLGetters.mls @@ -60,7 +60,7 @@ test2 :sjs test2() //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return test21() +//β”‚ test21(); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = fun //β”‚ Type: Int -> Int diff --git a/hkmc2/shared/src/test/mlscript/meta/ImporterTest.mls b/hkmc2/shared/src/test/mlscript/meta/ImporterTest.mls index 7a78953375..9d637e057c 100644 --- a/hkmc2/shared/src/test/mlscript/meta/ImporterTest.mls +++ b/hkmc2/shared/src/test/mlscript/meta/ImporterTest.mls @@ -8,7 +8,7 @@ :sjs hello() //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return "Hello!" +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = "Hello!" @@ -18,7 +18,7 @@ fun hello() = "Hello?" :sjs hello() //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return "Hello?" +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = "Hello?" diff --git a/hkmc2/shared/src/test/mlscript/opt/DeadObjRemoval.mls b/hkmc2/shared/src/test/mlscript/opt/DeadObjRemoval.mls index a9130444fb..0a00001bc0 100644 --- a/hkmc2/shared/src/test/mlscript/opt/DeadObjRemoval.mls +++ b/hkmc2/shared/src/test/mlscript/opt/DeadObjRemoval.mls @@ -11,7 +11,7 @@ f() //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Optimized IR |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ define f⁰ as fun fΒΉ() { return 42 }; return 42 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let f; f = function f() { return 42 }; return 42 +//β”‚ let f; f = function f() { return 42 }; //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 42 @@ -73,7 +73,6 @@ f() //β”‚ toString() { return runtime.render(this); } //β”‚ static [definitionMetadata] = ["object", "A"]; //β”‚ }); -//β”‚ return 42 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ > Hello //β”‚ = 42 diff --git a/hkmc2/shared/src/test/mlscript/parser/PrefixOps.mls b/hkmc2/shared/src/test/mlscript/parser/PrefixOps.mls index 25861b23e1..ce9b2271ed 100644 --- a/hkmc2/shared/src/test/mlscript/parser/PrefixOps.mls +++ b/hkmc2/shared/src/test/mlscript/parser/PrefixOps.mls @@ -20,16 +20,17 @@ //β”‚ β•‘ l.16: 1 //β”‚ ╙── ^ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return 3 +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 3 +//β”‚ :sjs 1 + 2 + 3 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return 6 +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 6 @@ -47,7 +48,7 @@ :sjs + //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let lambda; lambda = (undefined, function (arg1, arg2) { return arg1 + arg2 }); return lambda +//β”‚ let lambda; lambda = (undefined, function (arg1, arg2) { return arg1 + arg2 }); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = fun @@ -58,7 +59,7 @@ 1 * //β”‚ ╔══[WARNING] Pure expression in statement position -//β”‚ β•‘ l.58: 1 +//β”‚ β•‘ l.59: 1 //β”‚ ╙── ^ //β”‚ = fun @@ -87,7 +88,7 @@ fun (??) foo(x, y) = x + y :pe ?? 1 //β”‚ ╔══[PARSE ERROR] Expected end of input; found literal instead -//β”‚ β•‘ l.88: ?? 1 +//β”‚ β•‘ l.89: ?? 1 //β”‚ ╙── ^ //β”‚ = fun foo diff --git a/hkmc2/shared/src/test/mlscript/std/FingerTreeListTest.mls b/hkmc2/shared/src/test/mlscript/std/FingerTreeListTest.mls index 2b37824c51..24efc1b59a 100644 --- a/hkmc2/shared/src/test/mlscript/std/FingerTreeListTest.mls +++ b/hkmc2/shared/src/test/mlscript/std/FingerTreeListTest.mls @@ -103,12 +103,7 @@ if xs is //β”‚ if (runtime.Tuple.isArrayLike(xs1) && xs1.length >= 1) { //β”‚ element0$ = runtime.Tuple.get(xs1, 0); //β”‚ middleElements = runtime.Tuple.slice(xs1, 1, 0); -//β”‚ return globalThis.Object.freeze([ -//β”‚ element0$, -//β”‚ middleElements -//β”‚ ]) -//β”‚ } -//β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")); +//β”‚ } else { throw globalThis.Object.freeze(new globalThis.Error("match error")) } //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = [1, [2, 3]] diff --git a/hkmc2/shared/src/test/mlscript/std/PredefTest.mls b/hkmc2/shared/src/test/mlscript/std/PredefTest.mls index c82205e440..e6cd600961 100644 --- a/hkmc2/shared/src/test/mlscript/std/PredefTest.mls +++ b/hkmc2/shared/src/test/mlscript/std/PredefTest.mls @@ -103,7 +103,7 @@ tuple passing(1, 2, 3) of 4, 5, 6 :sjs ??? //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return Predef.notImplementedError +//β”‚ Predef.notImplementedError; //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ ═══[RUNTIME ERROR] Error: Not implemented diff --git a/hkmc2/shared/src/test/mlscript/tailrec/TailRecOpt.mls b/hkmc2/shared/src/test/mlscript/tailrec/TailRecOpt.mls index 46015ac3e9..913ad2ebd7 100644 --- a/hkmc2/shared/src/test/mlscript/tailrec/TailRecOpt.mls +++ b/hkmc2/shared/src/test/mlscript/tailrec/TailRecOpt.mls @@ -96,7 +96,7 @@ A.sum(20000) //β”‚ toString() { return runtime.render(this); } //β”‚ static [definitionMetadata] = ["class", "A"]; //β”‚ }); -//β”‚ return A1.sum(20000) +//β”‚ A1.sum(20000); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 200010000 diff --git a/hkmc2/shared/src/test/mlscript/ucs/future/SymbolicClass.mls b/hkmc2/shared/src/test/mlscript/ucs/future/SymbolicClass.mls index 2bddb7c0a2..2d36ae2217 100644 --- a/hkmc2/shared/src/test/mlscript/ucs/future/SymbolicClass.mls +++ b/hkmc2/shared/src/test/mlscript/ucs/future/SymbolicClass.mls @@ -34,7 +34,7 @@ new 1 :: 2 //β”‚ β•‘ ^ //β”‚ ╙── The 'new' keyword requires a statically known class; use the 'new!' operator for dynamic instantiation. //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let tmp; tmp = globalThis.Object.freeze(new 1()); return Cons1(tmp, 2) +//β”‚ let tmp; tmp = globalThis.Object.freeze(new 1()); Cons1(tmp, 2); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ ═══[RUNTIME ERROR] TypeError: 1 is not a constructor @@ -48,7 +48,7 @@ new (1 :: 2) //β”‚ β•‘ ^^^^^^ //β”‚ ╙── The 'new' keyword requires a statically known class; use the 'new!' operator for dynamic instantiation. //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ let tmp1; tmp1 = Cons1(1, 2); return globalThis.Object.freeze(new tmp1()) +//β”‚ let tmp1; tmp1 = Cons1(1, 2); globalThis.Object.freeze(new tmp1()); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ ═══[RUNTIME ERROR] TypeError: tmp1 is not a constructor diff --git a/hkmc2/shared/src/test/mlscript/ucs/general/JoinPoints.mls b/hkmc2/shared/src/test/mlscript/ucs/general/JoinPoints.mls index dfbe6719ef..f641248836 100644 --- a/hkmc2/shared/src/test/mlscript/ucs/general/JoinPoints.mls +++ b/hkmc2/shared/src/test/mlscript/ucs/general/JoinPoints.mls @@ -7,12 +7,9 @@ x => if x is 0 then 1 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ let lambda; //β”‚ lambda = (undefined, function (x) { -//β”‚ if (x === 0) { -//β”‚ return 1 -//β”‚ } +//β”‚ if (x === 0) { return 1 } //β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")); //β”‚ }); -//β”‚ return lambda //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = fun @@ -36,7 +33,6 @@ x => if x is [[0]] then 1 //β”‚ } //β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")); //β”‚ }); -//β”‚ return lambda1 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = fun diff --git a/hkmc2/shared/src/test/mlscript/ucs/general/LogicalConnectives.mls b/hkmc2/shared/src/test/mlscript/ucs/general/LogicalConnectives.mls index 9d57763640..96f2bb5583 100644 --- a/hkmc2/shared/src/test/mlscript/ucs/general/LogicalConnectives.mls +++ b/hkmc2/shared/src/test/mlscript/ucs/general/LogicalConnectives.mls @@ -25,12 +25,7 @@ fun test(x) = :sjs true and test(42) //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ if (true === true) { -//β”‚ Predef.print(42); -//β”‚ if (42 === true) { return true } -//β”‚ return false; -//β”‚ } -//β”‚ return false; +//β”‚ if (true === true) { Predef.print(42); } //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ > 42 //β”‚ = false @@ -38,7 +33,7 @@ true and test(42) :fixme true or test(42) //β”‚ ╔══[COMPILATION ERROR] Logical `or` is not yet supported. -//β”‚ β•‘ l.39: true or test(42) +//β”‚ β•‘ l.34: true or test(42) //β”‚ ╙── ^^^^^^^^^^^^^^^^ //β”‚ > 42 //β”‚ = false diff --git a/hkmc2/shared/src/test/mlscript/ucs/normalization/ExcessiveDeduplication.mls b/hkmc2/shared/src/test/mlscript/ucs/normalization/ExcessiveDeduplication.mls index a51f67d297..1509538cea 100644 --- a/hkmc2/shared/src/test/mlscript/ucs/normalization/ExcessiveDeduplication.mls +++ b/hkmc2/shared/src/test/mlscript/ucs/normalization/ExcessiveDeduplication.mls @@ -19,11 +19,9 @@ if x then 0 y then 0 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ if (x === true) { -//β”‚ return 0 +//β”‚ if (x !== true) { +//β”‚ if (y !== true) { throw globalThis.Object.freeze(new globalThis.Error("match error")) } //β”‚ } -//β”‚ if (y === true) { return 0 } -//β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 0 @@ -33,11 +31,9 @@ if x then 0 y then 1 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ if (x === true) { -//β”‚ return 0 +//β”‚ if (x !== true) { +//β”‚ if (y !== true) { throw globalThis.Object.freeze(new globalThis.Error("match error")) } //β”‚ } -//β”‚ if (y === true) { return 1 } -//β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")); //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 0 @@ -108,9 +104,10 @@ if false do () //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Optimized IR |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ return 123 //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ return 123 +//β”‚ //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = 123 +//β”‚ :patMatConsequentSharingThreshold 10 @@ -132,7 +129,7 @@ object Obj with :sjs if Obj.test do () //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| JS (unsanitized) |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” -//β”‚ Obj1.test; return runtime.Unit +//β”‚ Obj1.test; //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ > hi diff --git a/hkmc2/shared/src/test/mlscript/ucs/normalization/SimplePairMatches.mls b/hkmc2/shared/src/test/mlscript/ucs/normalization/SimplePairMatches.mls index d48457a8c0..0f4f6b8fe5 100644 --- a/hkmc2/shared/src/test/mlscript/ucs/normalization/SimplePairMatches.mls +++ b/hkmc2/shared/src/test/mlscript/ucs/normalization/SimplePairMatches.mls @@ -25,7 +25,6 @@ x => if x is Pair(A, B) then 1 //β”‚ } //β”‚ throw globalThis.Object.freeze(new globalThis.Error("match error")); //β”‚ }); -//β”‚ return lambda //β”‚ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”| Output |β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” //β”‚ = fun diff --git a/hkmc2AppsTests/src/test/scala/hkmc2/AppsCompileTestRunner.scala b/hkmc2AppsTests/src/test/scala/hkmc2/AppsCompileTestRunner.scala index f4657f154c..fe0354c336 100644 --- a/hkmc2AppsTests/src/test/scala/hkmc2/AppsCompileTestRunner.scala +++ b/hkmc2AppsTests/src/test/scala/hkmc2/AppsCompileTestRunner.scala @@ -14,7 +14,11 @@ end AppsCompileTestRunner object AppsCompileTestRunner: - given cctx: CompilerCtx = CompilerCtx.fresh(io.FileSystem.default) + private val workingDir = os.pwd + private val stdPath = TestFolders.compileTestDir(workingDir) + private val nodeModulesPath = workingDir / "node_modules" + + given cctx: CompilerCtx = + CompilerCtx.fresh(io.FileSystem.default, LocalModuleResolver(stdPath, S(nodeModulesPath))) end AppsCompileTestRunner - diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/DiffMakerTests.scala b/hkmc2DiffTests/src/test/scala/hkmc2/DiffMakerTests.scala index af32121f96..d59e1580c6 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/DiffMakerTests.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/DiffMakerTests.scala @@ -4,11 +4,15 @@ import java.nio.file.Files import org.scalatest.funsuite.AnyFunSuite +import hkmc2.utils.*, shorthands.* import io.{FileSystem, PlatformPath} import io.PlatformPath.given class DiffMakerTests extends AnyFunSuite: + private object NoModuleResolver extends ModuleResolver: + def tryResolveModulePath(path: Str): Opt[ModuleResolver.ResolvedModule] = + ModuleResolver.tryResolveUrl(path) private class StubDiffMaker( val cctx: CompilerCtx, val file: io.Path, @@ -25,7 +29,7 @@ class DiffMakerTests extends AnyFunSuite: val dir = os.Path(Files.createTempDirectory("diff-maker-tests").toString) val testFile = dir / s"Test-${System.nanoTime}.mls" os.write.over(testFile, source) - val compilerCtx = CompilerCtx.fresh(FileSystem.default) + val compilerCtx = CompilerCtx.fresh(FileSystem.default, NoModuleResolver) try val dm = new StubDiffMaker(compilerCtx, testFile, "Test") dm.run() diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/DiffTestRunner.scala b/hkmc2DiffTests/src/test/scala/hkmc2/DiffTestRunner.scala index 8edb6fd07a..a8e787f754 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/DiffTestRunner.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/DiffTestRunner.scala @@ -22,7 +22,6 @@ object DiffTestRunner: class State: - val cctx: CompilerCtx = CompilerCtx.fresh(io.FileSystem.default) val pwd = os.pwd @@ -37,6 +36,9 @@ object DiffTestRunner: // To be overridden in subproject-specific State classes def testDir: os.Path = dir + val nodeModulesPath = workingDir/"node_modules" + + val cctx: CompilerCtx = CompilerCtx.fresh(io.FileSystem.default, LocalModuleResolver(dir/"mlscript-compile", S(nodeModulesPath))) val validExt = Set("mls") diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala index 3ada72ad66..3aced40ef9 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala @@ -196,7 +196,9 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: val nestedScp = baseScp // val nestedScp = codegen.js.Scope(S(baseScp), curCtx.outer, collection.mutable.Map.empty) // * not needed - val importedSymbols: Set[ScopedSymbol] = pgrm.imports.iterator.map(_._1).toSet + val importedSymbols: Set[ScopedSymbol] = pgrm.imports.iterator.collect: + case ImportSpec(sym: ScopedSymbol, _, _) => sym + .toSet val exportedScoped = symbolsToPreserve.collect: case sym: ScopedSymbol if !importedSymbols.contains(sym) => sym diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala b/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala index 3121b532db..544c106265 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala @@ -34,7 +34,19 @@ class Watcher(dirs: Ls[File]): val completionTime = mutable.Map.empty[File, LocalDateTime] val fileHasher = FileHasher.DEFAULT_FILE_HASHER - given cctx: CompilerCtx = CompilerCtx.fresh(FileSystem.default) + val rootPath = os.pwd/os.up + val testDir = rootPath/"hkmc2"/"shared"/"src"/"test" + val preludePath = testDir/"mlscript"/"decls"/"Prelude.mls" + val predefPath = testDir/"mlscript-compile"/"Predef.mls" + val stdPath = testDir/"mlscript-compile" + val compilerPaths = new MLsCompiler.Paths: + val preludeFile = preludePath + val runtimeFile = testDir/"mlscript-compile"/"Runtime.mjs" + val runtimeSourceFile = testDir/"mlscript-compile"/"Runtime.mls" + val termFile = testDir/"mlscript-compile"/"Term.mjs" + val nodeModulesPath = rootPath/"node_modules" + + given cctx: CompilerCtx = CompilerCtx.fresh(FileSystem.default, LocalModuleResolver(stdPath, S(nodeModulesPath))) val watcher: DirectoryWatcher = DirectoryWatcher.builder() .logger(org.slf4j.helpers.NOPLogger.NOP_LOGGER) diff --git a/hkmc2NofibTests/src/test/scala/hkmc2/NofibCompileTestRunner.scala b/hkmc2NofibTests/src/test/scala/hkmc2/NofibCompileTestRunner.scala index 7626bd6358..4068cc7dc8 100644 --- a/hkmc2NofibTests/src/test/scala/hkmc2/NofibCompileTestRunner.scala +++ b/hkmc2NofibTests/src/test/scala/hkmc2/NofibCompileTestRunner.scala @@ -14,7 +14,11 @@ end NofibCompileTestRunner object NofibCompileTestRunner: - given cctx: CompilerCtx = CompilerCtx.fresh(io.FileSystem.default) + private val workingDir = os.pwd + private val stdPath = TestFolders.compileTestDir(workingDir) + private val nodeModulesPath = workingDir / "node_modules" + + given cctx: CompilerCtx = + CompilerCtx.fresh(io.FileSystem.default, LocalModuleResolver(stdPath, S(nodeModulesPath))) end NofibCompileTestRunner - diff --git a/hkmc2PackagesTest/src/test/scala/hkmc2/PackageManifest.scala b/hkmc2PackagesTest/src/test/scala/hkmc2/PackageManifest.scala new file mode 100644 index 0000000000..3171272c3a --- /dev/null +++ b/hkmc2PackagesTest/src/test/scala/hkmc2/PackageManifest.scala @@ -0,0 +1,61 @@ +package hkmc2 + +import hkmc2.utils.*, shorthands.* + +case class PackageManifest( + name: Str, + main: Str, + moduleName: Str, + vendors: Ls[VendorManifest], +) + +/** Represents an entry in the `vendors` field of the manifest. */ +case class VendorManifest(prefix: Str, path: Str, files: Ls[Str]) + +object PackageManifest: + + /** Parse a package's manifest.json file. */ + def read(packageDir: os.Path): PackageManifest = + val manifestPath = packageDir / "manifest.json" + val json = ujson.read(os.read(manifestPath)) + val obj = json match + case ujson.Obj(value) => value + case _ => throw new Exception(s"Expected JSON object in $manifestPath") + PackageManifest( + stringField(obj, "name", manifestPath), + stringField(obj, "main", manifestPath), + stringField(obj, "moduleName", manifestPath), + vendors(obj, manifestPath), + ) + + /** Parse a required string field. */ + private def stringField(obj: collection.Map[Str, ujson.Value], name: Str, manifestPath: os.Path): Str = + obj.get(name) match + case S(ujson.Str(value)) => value + case S(_) => throw new Exception(s"Expected string field '$name' in $manifestPath") + case N => throw new Exception(s"Missing string field '$name' in $manifestPath") + + /** Parse the optional vendors array. */ + private def vendors(obj: collection.Map[Str, ujson.Value], manifestPath: os.Path): Ls[VendorManifest] = + obj.get("vendors") match + case S(ujson.Arr(values)) => + values.toList.map: + case ujson.Obj(vendor) => + VendorManifest( + stringField(vendor, "prefix", manifestPath), + stringField(vendor, "path", manifestPath), + stringArrayField(vendor, "files", manifestPath), + ) + case _ => throw new Exception(s"Expected object entries in 'vendors' in $manifestPath") + case S(_) => throw new Exception(s"Expected array field 'vendors' in $manifestPath") + case N => Nil + + /** Parse a required string array field. */ + private def stringArrayField(obj: collection.Map[Str, ujson.Value], name: Str, manifestPath: os.Path): Ls[Str] = + obj.get(name) match + case S(ujson.Arr(values)) => + values.toList.map: + case ujson.Str(value) => value + case _ => throw new Exception(s"Expected string entries in '$name' in $manifestPath") + case S(_) => throw new Exception(s"Expected string array field '$name' in $manifestPath") + case N => throw new Exception(s"Missing string array field '$name' in $manifestPath") diff --git a/hkmc2PackagesTest/src/test/scala/hkmc2/PackageModuleResolver.scala b/hkmc2PackagesTest/src/test/scala/hkmc2/PackageModuleResolver.scala new file mode 100644 index 0000000000..65ed80f417 --- /dev/null +++ b/hkmc2PackagesTest/src/test/scala/hkmc2/PackageModuleResolver.scala @@ -0,0 +1,290 @@ +package hkmc2 + +import scala.collection.immutable.ListMap +import scala.collection.mutable.{LinkedHashMap as MutLinkedHashMap, Map as MutMap, Queue as MutQueue, Set as MutSet} + +import hkmc2.utils.*, shorthands.* +import io.PlatformPath.given + +import hkmc2.io.FileSystem + +/** A source artifact and the package-local file generated or copied from it. */ +case class VendoredFile(source: os.Path, target: os.Path) + +/** A manifest vendor entry after resolving source and target roots. */ +case class VendorRoot(prefix: Str, root: os.Path, targetRoot: os.Path, patterns: Ls[Str]) + +// We may use a different module resolver for URL modules in browsers. For +// example, `import "https://esm.sh/nanoid"` should be accepted. + +class PackageModuleResolver( + packageDir: os.Path, + manifest: PackageManifest, + nodeModulesPath: Opt[io.Path], + standardLibraryRoot: os.Path, +)(using fs: io.FileSystem) extends LocalModuleResolver(Nil, nodeModulesPath): + import PackageModuleResolver.* + + private case class PackageScope(root: os.Path, vendors: Ls[VendorRoot]) + + private val packageScopes = buildPackageScopes() + private val rootScope = packageScopes(identityPath(packageDir)) + private val allVendorRoots = packageScopes.valuesIterator.flatMap(_.vendors).toList + private val uniqueVendorRoots = allVendorRoots.distinctBy(vendor => identityPath(vendor.root)) + private val vendorRootsByDepthDesc = uniqueVendorRoots.sortBy(vendor => -vendor.root.segmentCount) + private val packageScopesByDepthDesc = packageScopes.valuesIterator.toList.sortBy(scope => -scope.root.segmentCount) + + private val closure = collectVendorClosure() + + val vendoredSources: Ls[VendoredFile] = closure.mlsTargets.toList.map: + case (source, target) => VendoredFile(source, target) + val copiedVendorFiles: Ls[VendoredFile] = closure.jsTargets.toList.map: + case (source, target) => VendoredFile(source, target) + + /** Tell the compiler where a vendored source should emit its .mjs output. */ + override def targetPathForSource(sourcePath: io.Path): Opt[io.Path] = + val source: os.Path = sourcePath + closure.targetForSource.get(source) match + case Some(target) => S(target) + case None => N + + /** Resolve package vendor prefixes before falling back to local/node resolution. */ + override def tryResolveModulePath(path: Str): Opt[ModuleResolver.ResolvedModule] = + tryResolveModulePath(path, packageDir) + + /** Resolve package vendor prefixes using the manifest of the importing file. */ + override def tryResolveModulePath(path: Str, from: io.Path): Opt[ModuleResolver.ResolvedModule] = + tryVendorFile(path, scopeForDirectory(from)) orElse super.tryResolveModulePath(path) + + /** Resolve a prefixed import to its source file and generated target path. */ + private def tryVendorFile(rawPath: Str, scope: PackageScope): Opt[ModuleResolver.ResolvedModule] = + scope.vendors.iterator.collectFirst: + case vendor if rawPath.startsWith(vendor.prefix) => + val relativePath = os.RelPath(rawPath.drop(vendor.prefix.length)) + val source = vendor.root / relativePath + closure.targetForSource.get(source) match + case Some(target) => S(ModuleResolver.ResolvedModule.File(source, target, source.baseName)) + case None => N + .flatten + + /** Collected vendored files, preserving manifest/discovery order. */ + private case class VendorClosure( + mlsTargets:ListMap[os.Path, os.Path], + jsTargets:ListMap[os.Path, os.Path], + targetForSource: Map[os.Path, os.Path], + ) + + /** Build the vendored MLscript closure and static JavaScript asset set. */ + private def collectVendorClosure(): VendorClosure = + val mlsTargets = MutLinkedHashMap.empty[os.Path, os.Path] + val jsTargets = MutLinkedHashMap.empty[os.Path, os.Path] + val copiedTargetPaths = MutSet.empty[os.Path] + val targetForSource = MutMap.empty[os.Path, os.Path] + val pendingMls = MutQueue.empty[os.Path] + + // Add one discovered MLscript vendor file and enqueue it for import scanning. + def include(path: os.Path): Unit = + ownerFor(path) match + case S(vendor) => + if !os.exists(path) then + throw new Exception(s"Vendored import does not exist: $path") + path.ext match + case "mls" => + val target = targetFor(vendor, path) + if !mlsTargets.contains(path) then + mlsTargets += path -> target + targetForSource += path -> target + pendingMls.enqueue(path) + case "mjs" => + val sourceMls = path / os.up / s"${path.baseName}.mls" + if os.exists(sourceMls) then + include(sourceMls) + targetForSource += path -> targetFor(vendor, sourceMls) + else + includeStatic(vendor, path) + case "js" => + includeStatic(vendor, path) + case _ => () + case N => () + + def includeStatic(vendor: VendorRoot, path: os.Path): Unit = + val target = targetFor(vendor, path) + if !jsTargets.contains(path) && !copiedTargetPaths(target) then + jsTargets += path -> target + copiedTargetPaths += target + targetForSource += path -> target + + rootScope.vendors.foreach: vendor => + matchedVendorFiles(vendor).foreach(include) + + while pendingMls.nonEmpty do + val source = pendingMls.dequeue() + importLiterals(source).foreach: rawImport => + resolveSourceImport(source, rawImport).foreach(include) + + uniqueVendorRoots.foreach: vendor => + staticJavaScriptFiles(vendor).foreach: source => + includeStatic(vendor, source) + + val compiledTargets = mlsTargets.values.toSet + val copiedJsTargets = jsTargets.filterNot: + case (_, target) => compiledTargets.contains(target) + + VendorClosure( + collection.immutable.ListMap.from(mlsTargets), + collection.immutable.ListMap.from(copiedJsTargets), + targetForSource.toMap, + ) + + /** Expand manifest file patterns under a vendor root. */ + private def matchedVendorFiles(vendor: VendorRoot): Ls[os.Path] = + val (literalPatterns, globPatterns) = vendor.patterns.partition(pattern => !hasGlobMeta(pattern)) + val literalFiles = literalPatterns.map(pattern => vendor.root / os.RelPath(pattern)).filter(os.isFile) + if globPatterns.isEmpty then literalFiles + else + import java.nio.file.FileSystems + val fileSystem = FileSystems.getDefault + val matchers = globPatterns.map(pattern => fileSystem.getPathMatcher(s"glob:$pattern")) + val globFiles = os.walk(vendor.root).iterator.filter: file => + if os.isFile(file) then + val relativePath = vendor.root.toNIO.relativize(file.toNIO) + matchers.exists(_.matches(relativePath)) + else false + .toList + literalFiles ::: globFiles + + private def hasGlobMeta(pattern: Str): Boolean = + pattern.exists: ch => + ch == '*' || ch == '?' || ch == '[' || ch == ']' || ch == '{' || ch == '}' + + /** Collect all JavaScript assets under a vendor root without scanning them. */ + private def staticJavaScriptFiles(vendor: VendorRoot): Ls[os.Path] = + os.walk(vendor.root).iterator.filter: file => + os.isFile(file) && + !file.startsWith(vendor.root / "vendors") && + (file.ext === "js" || file.ext === "mjs") && + !os.exists(file / os.up / s"${file.baseName}.mls") + .toList + + /** Extract MLscript import string literals for rudimentary closure tracking. */ + private def importLiterals(file: os.Path): Ls[Str] = + // TODO: This regex is deliberately rudimentary. Reuse the parsed/elaborated + // trees cached by CompilerCtx instead, so vendoring follows real imports. + val ImportLiteral = "(?m)^\\s*import\\s+\"([^\"]+)\"".r + ImportLiteral.findAllMatchIn(os.read(file)).map(_.group(1)).toList + + /** Resolve an import seen while scanning a vendored source file. */ + private def resolveSourceImport(currentFile: os.Path, rawPath: Str): Opt[os.Path] = + if rawPath.startsWith("./") || rawPath.startsWith("../") then + S(currentFile / os.up / os.RelPath(rawPath)) + else if rawPath.startsWith("/") then + S(os.Path(rawPath)) + else + scopeForDirectory(currentFile / os.up).vendors.find(vendor => rawPath.startsWith(vendor.prefix)) match + case Some(vendor) => S(vendor.root / os.RelPath(rawPath.drop(vendor.prefix.length))) + case None => N + + /** Find which declared vendor root owns a filesystem path. */ + private def ownerFor(path: os.Path): Opt[VendorRoot] = + vendorRootsByDepthDesc.find(vendor => path.startsWith(vendor.root)) match + case Some(vendor) => S(vendor) + case None => N + + /** Compute the package-local generated/copied path for a vendor file. */ + private def targetFor(vendor: VendorRoot, source: os.Path): os.Path = + val relativePath = source.relativeTo(vendor.root) + val target = vendor.targetRoot / relativePath + if source.ext === "mls" then target / os.up / s"${target.baseName}.mjs" + else target + + private def scopeForDirectory(from: io.Path): PackageScope = + val dir: os.Path = from + packageScopesByDepthDesc + .find(scope => dir.startsWith(scope.root)) + .getOrElse(rootScope) + + private def buildPackageScopes(): ListMap[Str, PackageScope] = + val scopes = MutLinkedHashMap.empty[Str, PackageScope] + val queued = MutSet.empty[Str] + val checkedManifests = MutSet.empty[Str] + val pending = MutQueue.empty[(os.Path, PackageManifest)] + val canonicalTargets = MutLinkedHashMap.empty[Str, os.Path] + + def canonicalTarget(sourceRoot: os.Path, suggestedTarget: os.Path): os.Path = + canonicalTargets.getOrElseUpdate(identityPath(sourceRoot), suggestedTarget) + + def mkVendorRoot(ownerRoot: os.Path, vendor: VendorManifest): VendorRoot = + val sourceRoot = resolvePackagePath(ownerRoot, vendor.path) + VendorRoot( + vendor.prefix, + sourceRoot, + canonicalTarget(sourceRoot, vendorTargetRoot(packageDir, vendor.prefix)), + vendor.files, + ) + + def withCompilerSupport(roots: Ls[VendorRoot]): Ls[VendorRoot] = + val supportFiles = Ls(RuntimeSourceFile, TermSourceFile) + roots.find(_.prefix === StandardLibraryPrefix) match + case S(_) => + roots.map: + case root if root.prefix === StandardLibraryPrefix => + root.copy(patterns = (supportFiles ::: root.patterns).distinct) + case root => root + case N => + roots :+ VendorRoot( + StandardLibraryPrefix, + standardLibraryRoot, + canonicalTarget(standardLibraryRoot, vendorTargetRoot(packageDir, StandardLibraryPrefix)), + supportFiles, + ) + + def enqueue(root: os.Path): Unit = + val key = identityPath(root) + if !queued(key) && !scopes.contains(key) && !checkedManifests(key) then + checkedManifests += key + manifestAt(root).foreach: manifest => + queued += key + pending.enqueue(root -> manifest) + + checkedManifests += identityPath(packageDir) + queued += identityPath(packageDir) + pending.enqueue(packageDir -> manifest) + + while pending.nonEmpty do + val (root, packageManifest) = pending.dequeue() + val key = identityPath(root) + if !scopes.contains(key) then + val vendors = withCompilerSupport(packageManifest.vendors.map(mkVendorRoot(root, _))) + scopes += key -> PackageScope(root, vendors) + vendors.foreach(vendor => enqueue(vendor.root)) + + collection.immutable.ListMap.from(scopes) + + private def manifestAt(root: os.Path): Opt[PackageManifest] = + if os.exists(root / "manifest.json") then S(PackageManifest.read(root)) + else N + + private def identityPath(path: os.Path): Str = + path.toNIO.normalize.toString + +object PackageModuleResolver: + val StandardLibraryPrefix: Str = "std/" + private val RuntimeSourceFile: Str = "Runtime.mls" + private val TermSourceFile: Str = "Term.mls" + + def runtimeTarget(packageDir: os.Path): os.Path = + vendorTargetRoot(packageDir, StandardLibraryPrefix) / "Runtime.mjs" + + def termTarget(packageDir: os.Path): os.Path = + vendorTargetRoot(packageDir, StandardLibraryPrefix) / "Term.mjs" + + /** Resolve a manifest path relative to the package directory. */ + private def resolvePackagePath(packageDir: os.Path, path: Str): os.Path = + if path.startsWith("/") then os.Path(path) + else packageDir / os.RelPath(path) + + /** Compute the generated vendors/ root for a vendor prefix. */ + private def vendorTargetRoot(packageDir: os.Path, prefix: Str): os.Path = + val normalizedPrefix = prefix.stripSuffix("/") + if normalizedPrefix.isEmpty then packageDir / "vendors" + else packageDir / "vendors" / os.RelPath(normalizedPrefix) diff --git a/hkmc2PackagesTest/src/test/scala/hkmc2/PackageTestRunner.scala b/hkmc2PackagesTest/src/test/scala/hkmc2/PackageTestRunner.scala new file mode 100644 index 0000000000..7b635aa3ec --- /dev/null +++ b/hkmc2PackagesTest/src/test/scala/hkmc2/PackageTestRunner.scala @@ -0,0 +1,108 @@ +package hkmc2 + +import org.scalatest.{funspec, ParallelTestExecution} + +import hkmc2.utils.*, shorthands.* +import io.PlatformPath.given + +import hkmc2.io.FileSystem + +/** + * A simple test runner that compiles packages written in MLscript. + */ +class PackageTestRunner + extends funspec.AnyFunSpec + // with ParallelTestExecution // Support parallel compilation in the future. + // with TimeLimitedTests // Support time limits when necessary. +: + import PackageTestRunner.* + import PackageTestRunner.given + + private val inParallel = isInstanceOf[ParallelTestExecution] + + for packageDir <- os.list(packagesDir).filter(os.isDir) do + val allFiles = os.walk(packageDir) + .filter(os.isFile) + .filter(_.ext == "mls") + .filter(file => !file.startsWith(packageDir / "vendors")) + .toSeq + val packageName = packageDir.baseName + val manifest = PackageManifest.read(packageDir) + val moduleResolver = PackageModuleResolver(packageDir, manifest, S(nodeModulesPath), stdlibDir) + val vendoredSources = moduleResolver.vendoredSources.toSeq + val copiedVendorFiles = moduleResolver.copiedVendorFiles.toSeq + + // The compiler context is created per package to avoid interference. + given cctx: CompilerCtx = CompilerCtx.fresh(fs, moduleResolver) + given Config = configForPackage(packageName) + + val wrap: (=> Unit) => Unit = body => PackageTestRunner.synchronized(body) + val report = ReportFormatter(System.out.println, colorize = true, wrap = Some(wrap)) + val compiler = MLsCompiler(pathsForPackage(packageDir), mkRaise = report.mkRaise) + + describe(s"$packageName (${"file" countBy allFiles.size})"): + + if vendoredSources.nonEmpty || copiedVendorFiles.nonEmpty then + it("vendors"): + os.remove.all(packageDir / "vendors") + copiedVendorFiles.foreach: file => + os.makeDir.all(file.target / os.up) + os.copy.over(file.source, file.target) + + vendoredSources.foreach: file => + os.makeDir.all(file.target / os.up) + PackageTestRunner.synchronized: + println(s"Vendoring: [${fansi.Bold.On(packageName)}] ${fansi.Color.Green(displayPath(file.source))}") + compiler.compileModule(file.source, S(file.target)) + assert(os.exists(file.target), s"Expected vendored artifact at ${file.target}") + + copiedVendorFiles.foreach: file => + assert(os.exists(file.target), s"Expected copied vendor artifact at ${file.target}") + + allFiles.foreach: file => + val relativeName = file.relativeTo(packageDir).toString() + + it(relativeName): + + PackageTestRunner.synchronized: + println(s"Compiling: [${fansi.Bold.On(packageName)}] ${fansi.Color.Green(relativeName)}") + + assert(true, s"Placeholder test for package: $relativeName") + + compiler.compileModule(file) + + if report.badLines.nonEmpty then + fail(s"Unexpected diagnostic at: " + + report.badLines.distinct.sorted + .map("\n\t"+relativeName+"."+file.ext+":"+_).mkString(", ")) +end PackageTestRunner + +object PackageTestRunner: + + val mainTestDir = TestFolders.mainTestDir(os.pwd) + val packagesDir = TestFolders.packagesTestDir(os.pwd) + val stdlibDir = mainTestDir / "mlscript-compile" + + def displayPath(path: os.Path): Str = + if path.startsWith(mainTestDir) then path.relativeTo(mainTestDir).toString + else if path.startsWith(os.pwd) then path.relativeTo(os.pwd).toString + else path.toString + + def pathsForPackage(packageDir: os.Path): MLsCompiler.Paths = new MLsCompiler.Paths: + val preludeFile = mainTestDir / "mlscript" / "decls" / "Prelude.mls" + val runtimeFile = PackageModuleResolver.runtimeTarget(packageDir) + val runtimeSourceFile = stdlibDir / "Runtime.mls" + val termFile = PackageModuleResolver.termTarget(packageDir) + + def configForPackage(packageName: Str): Config = + val defaultConfig = Config.default(mainTestDir) + // Definition lifting currently changes callback interop in generated JS for web IDE code. + // Keep this package on the pre-lifting codegen path until that compiler bug is fixed. + if packageName == "web-ide" then defaultConfig.copy(liftDefns = N) + else defaultConfig + + val nodeModulesPath = os.pwd / "node_modules" + + given fs: FileSystem = io.FileSystem.default + +end PackageTestRunner diff --git a/hkmc2WasmTests/src/test/scala/hkmc2/WasmCompileTestRunner.scala b/hkmc2WasmTests/src/test/scala/hkmc2/WasmCompileTestRunner.scala index 698a389bd6..25126f1b37 100644 --- a/hkmc2WasmTests/src/test/scala/hkmc2/WasmCompileTestRunner.scala +++ b/hkmc2WasmTests/src/test/scala/hkmc2/WasmCompileTestRunner.scala @@ -14,6 +14,11 @@ end WasmCompileTestRunner object WasmCompileTestRunner: - given cctx: CompilerCtx = CompilerCtx.fresh(io.FileSystem.default) + private val workingDir = os.pwd + private val stdPath = TestFolders.compileTestDir(workingDir) + private val nodeModulesPath = workingDir / "node_modules" + + given cctx: CompilerCtx = + CompilerCtx.fresh(io.FileSystem.default, LocalModuleResolver(stdPath, S(nodeModulesPath))) end WasmCompileTestRunner diff --git a/wrangler.toml b/wrangler.toml new file mode 100644 index 0000000000..dbb75dadeb --- /dev/null +++ b/wrangler.toml @@ -0,0 +1,7 @@ +name = "mlscript-web-ide" +compatibility_date = "2026-05-18" +workers_dev = false +preview_urls = true + +[assets] +directory = "hkmc2/shared/src/test/mlscript-packages/web-ide"