From 6ed127ad27bfdc066471a7e1eb9b6b8091d9d4b9 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Wed, 7 Oct 2026 01:07:15 +0300 Subject: [PATCH 1/7] feat(Compilers): add Nim language support --- README.md | 2 +- docs/docs/languages/nim-wasm.mdx | 110 +++++++++++ docs/docs/languages/nim.mdx | 92 +++++++++ docs/src/components/LanguageSliders.tsx | 2 + docs/src/components/TemplateList.tsx | 2 + e2e/specs/starter.spec.ts | 92 +++++++++ functions/vendors/templates.js | 2 + scripts/build.js | 1 + server/php/inc/starter-templates.json | 2 + src/livecodes/UI/command-menu-actions.ts | 2 + src/livecodes/assets/templates/nim.svg | 1 + src/livecodes/html/language-info.html | 62 ++++++ .../locales/en/language-info.lokalise.json | 24 +++ .../i18n/locales/en/language-info.ts | 10 + .../i18n/locales/en/translation.lokalise.json | 8 + src/livecodes/i18n/locales/en/translation.ts | 2 + src/livecodes/languages/languages.ts | 3 + .../nim/__tests__/lang-nim-wasm.spec.ts | 40 ++++ src/livecodes/languages/nim/index.ts | 2 + .../languages/nim/lang-nim-wasm-script.ts | 177 ++++++++++++++++++ src/livecodes/languages/nim/lang-nim-wasm.ts | 32 ++++ src/livecodes/languages/nim/lang-nim.ts | 40 ++++ src/livecodes/languages/nim/nim-editor.ts | 47 +++++ src/livecodes/models.ts | 1 + src/livecodes/templates/starter/index.ts | 4 + .../templates/starter/nim-starter.ts | 52 +++++ .../templates/starter/nim-wasm-starter.ts | 78 ++++++++ src/livecodes/vendors.ts | 2 + src/sdk/models.ts | 9 + storybook/_stories/EmbedOptions/template.ts | 2 + vendor-licenses.md | 2 + 31 files changed, 904 insertions(+), 1 deletion(-) create mode 100644 docs/docs/languages/nim-wasm.mdx create mode 100644 docs/docs/languages/nim.mdx create mode 100644 src/livecodes/assets/templates/nim.svg create mode 100644 src/livecodes/languages/nim/__tests__/lang-nim-wasm.spec.ts create mode 100644 src/livecodes/languages/nim/index.ts create mode 100644 src/livecodes/languages/nim/lang-nim-wasm-script.ts create mode 100644 src/livecodes/languages/nim/lang-nim-wasm.ts create mode 100644 src/livecodes/languages/nim/lang-nim.ts create mode 100644 src/livecodes/languages/nim/nim-editor.ts create mode 100644 src/livecodes/templates/starter/nim-starter.ts create mode 100644 src/livecodes/templates/starter/nim-wasm-starter.ts diff --git a/README.md b/README.md index 08af338fb2..b5c7177fe9 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ A [feature-rich](https://livecodes.io/docs/features/), open-source, **client-sid [![LiveCodes: npm version](https://img.shields.io/npm/v/livecodes)](https://www.npmjs.com/package/livecodes) [![LiveCodes: npm downloads](https://img.shields.io/npm/dm/livecodes)](https://www.npmjs.com/package/livecodes) [![LiveCodes: jsdelivr downloads](https://data.jsdelivr.com/v1/package/npm/livecodes/badge?style=rounded)](https://www.jsdelivr.com/package/npm/livecodes) -[![LiveCodes: languages](https://img.shields.io/badge/languages-116-blue)](https://livecodes.io/docs/languages/) +[![LiveCodes: languages](https://img.shields.io/badge/languages-118-blue)](https://livecodes.io/docs/languages/) [![LiveCodes: docs](https://img.shields.io/badge/Documentation-575757?logo=gitbook&logoColor=white)](https://livecodes.io/docs/) [![LiveCodes: llms.txt](https://img.shields.io/badge/llms.txt-575757?logo=googledocs&logoColor=white)](https://livecodes.io/docs/llms.txt) [![LiveCodes: llms-full.txt](https://img.shields.io/badge/llms--full.txt-575757?logo=googledocs&logoColor=white)](https://livecodes.io/docs/llms-full.txt) diff --git a/docs/docs/languages/nim-wasm.mdx b/docs/docs/languages/nim-wasm.mdx new file mode 100644 index 0000000000..2c296c67fb --- /dev/null +++ b/docs/docs/languages/nim-wasm.mdx @@ -0,0 +1,110 @@ +# Nim (Wasm) + +[Nim](https://nim-lang.org/) is a statically typed, compiled systems programming language. + +In LiveCodes, Nim runs in the browser using the [Nim compiler](https://nim-lang.org/) compiled to WebAssembly. This target (Nim's `c` backend) compiles the program to C and then to WebAssembly using the [Clang](https://clang.llvm.org/) toolchain, the same one used for [C (Wasm)](./c-wasm.mdx), entirely client-side. + +## Usage + +Demo: + +import LiveCodes from '../../src/components/LiveCodes.tsx'; + +export const nimWasmConfig = { + activeEditor: 'script', + script: { + language: 'nim-wasm', + content: `echo "Hello, World!"`, + }, + mode: 'simple', + editor: 'auto', + tools: { + status: 'full', + }, +}; + + + +### Communication with JavaScript + +The Nim code is compiled and run in a web worker. The following helpers are available in the browser global `livecodes.nimWasm` object: + +- `livecodes.nimWasm.input`: The standard input passed to the program. +- `livecodes.nimWasm.loaded`: A promise that resolves when the compiler is loaded and the program has run once. +- `livecodes.nimWasm.output`: The program's output. +- `livecodes.nimWasm.error`: The compiler diagnostics, if the code failed to compile. +- `livecodes.nimWasm.exitCode`: The program's exit code. +- `livecodes.nimWasm.run`: A function that compiles and runs the code with new input. It takes a string as input and returns a promise that resolves with an object containing `output`, `error` and `exitCode`. + +Example: + + + +## Language Info + +### Name + +`nim-wasm` + +### Aliases / Extensions + +`nimwasm`, `nim-wasm`, `wasm.nim` + +### Editor + +`script` + +## Compiler + +The [Nim compiler](https://nim-lang.org/) (Nim's `c` backend) compiled to WebAssembly. The generated C is compiled and linked by the [Clang](https://clang.llvm.org/) toolchain of [@live-codes/clang-wasm](https://www.npmjs.com/package/@live-codes/clang-wasm). + +### Version + +Nim `v2.2.4`, Clang `v22.1.8`. + +## Custom Settings + +The compiler can be configured using [custom settings](../advanced/custom-settings.mdx) added to the property `nim-wasm`: + +| Property | Type | Description | +| --- | --- | --- | +| `compileArgs` | `string[]` | Extra Nim flags, e.g. `["--define:release2"]`. | +| `args` | `string[]` | Command-line arguments passed to the program. | + +```json title="Custom Settings" +{ + "nim-wasm": { + "compileArgs": ["--define:release2"], + "args": ["--name", "LiveCodes"] + } +} +``` + +## Live Reload + +By default, new code changes are sent to the result page without a full page reload, avoiding the need to re-download the compiler. Adding the code comment `# __livecodes_reload__` forces a full page reload. + +## Example Usage + +```nim +import std/strutils + +echo "Hello, World!" + +let name = stdin.readLine() +echo "Hello, ", name, "!" +``` + +This target supports standard input, command-line arguments and real exit codes. A program that reads standard input gets an end-of-input error when it is left blank. + +## Starter Template + +https://livecodes.io/?template=nim-wasm + +## Links + +- [Nim official website](https://nim-lang.org/) +- [Nim manual](https://nim-lang.org/docs/manual.html) +- [Nim standard library](https://nim-lang.org/docs/lib.html) +- [@live-codes/nim-wasm](https://www.npmjs.com/package/@live-codes/nim-wasm) +- [C (Wasm)](./c-wasm.mdx) in LiveCodes diff --git a/docs/docs/languages/nim.mdx b/docs/docs/languages/nim.mdx new file mode 100644 index 0000000000..57894e8dd7 --- /dev/null +++ b/docs/docs/languages/nim.mdx @@ -0,0 +1,92 @@ +# Nim + +[Nim](https://nim-lang.org/) is a statically typed, compiled systems programming language. + +In LiveCodes, Nim runs in the browser using the [Nim compiler](https://nim-lang.org/) compiled to WebAssembly, through Nim's JavaScript backend. The compiled program runs in the result page, so it can drive the DOM directly (e.g. through the `dom` and `jsffi` modules). + +> For Nim's WebAssembly backend, which supports standard input, command-line arguments and real exit codes, see [Nim (Wasm)](./nim-wasm.mdx). + +## Usage + +Demo: + +import LiveCodes from '../../src/components/LiveCodes.tsx'; + +export const nimConfig = { + activeEditor: 'script', + script: { + language: 'nim', + content: `echo "Hello, World!"`, + }, + mode: 'simple', + editor: 'auto', + tools: { + status: 'full', + }, +}; + + + +### Communication with the result page + +Nim's JavaScript backend emits one self-sufficient program that runs in the result page. It has a document and a console, so `echo` writes to the console, and the browser modules that ship with the compiler (`dom`, `jsffi`, `jsconsole`, `jscore`, `jsre` and `asyncjs`) are on its search path: + +```nim +import dom + +document.title = "Nim in LiveCodes" +let element = document.createElement("p") +element.innerText = "Hello from Nim!" +document.body.appendChild(element) +``` + +Example: + + + +## Language Info + +### Name + +`nim` + +### Aliases / Extensions + +`nim`, `nims`, `nimble` + +### Editor + +`script` + +## Compiler + +The [Nim compiler](https://nim-lang.org/) (Nim's `js` backend) compiled to WebAssembly, packaged as [@live-codes/nim-wasm](https://www.npmjs.com/package/@live-codes/nim-wasm). It is compiled with `execute: false`, and the emitted program runs in the result page. + +### Version + +Nim `v2.2.4`. + +## Live Reload + +By default, new code changes are sent to the result page without a full page reload, avoiding the need to re-download the compiler. Adding the code comment `# __livecodes_reload__` forces a full page reload. + +## Example Usage + +```nim +import std/strutils + +echo "Hello, World!" +echo "2 + 2 = ", 2 + 2 +``` + +## Starter Template + +https://livecodes.io/?template=nim + +## Links + +- [Nim official website](https://nim-lang.org/) +- [Nim manual](https://nim-lang.org/docs/manual.html) +- [Nim standard library](https://nim-lang.org/docs/lib.html) +- [@live-codes/nim-wasm](https://www.npmjs.com/package/@live-codes/nim-wasm) +- [Nim (Wasm)](./nim-wasm.mdx) in LiveCodes diff --git a/docs/src/components/LanguageSliders.tsx b/docs/src/components/LanguageSliders.tsx index 710cb7d960..13dd3005a0 100644 --- a/docs/src/components/LanguageSliders.tsx +++ b/docs/src/components/LanguageSliders.tsx @@ -78,6 +78,8 @@ export default function Sliders() { { name: 'rescript', title: 'ReScript' }, { name: 'reason', title: 'Reason' }, { name: 'ocaml', title: 'OCaml' }, + { name: 'nim', title: 'Nim' }, + { name: 'nim-wasm', title: 'Nim (Wasm)' }, { name: 'python', title: 'Python' }, { name: 'python-wasm', title: 'Python (Wasm)' }, { name: 'r', title: 'R' }, diff --git a/docs/src/components/TemplateList.tsx b/docs/src/components/TemplateList.tsx index 0620493154..2bf4915b38 100644 --- a/docs/src/components/TemplateList.tsx +++ b/docs/src/components/TemplateList.tsx @@ -37,6 +37,8 @@ const templates = [ { name: 'rescript', title: 'ReScript Starter', thumbnail: 'rescript.png' }, { name: 'reason', title: 'Reason Starter', thumbnail: 'reason.svg' }, { name: 'ocaml', title: 'Ocaml Starter', thumbnail: 'ocaml.svg' }, + { name: 'nim', title: 'Nim Starter', thumbnail: 'nim.svg' }, + { name: 'nim-wasm', title: 'Nim (Wasm) Starter', thumbnail: 'nim.svg' }, { name: 'python', title: 'Python Starter', thumbnail: 'python.svg' }, { name: 'python-wasm', title: 'Python (Wasm) Starter', thumbnail: 'python.svg' }, { name: 'r', title: 'R Starter', thumbnail: 'r.svg' }, diff --git a/e2e/specs/starter.spec.ts b/e2e/specs/starter.spec.ts index 8dae79225d..1cd4c3b6a5 100644 --- a/e2e/specs/starter.spec.ts +++ b/e2e/specs/starter.spec.ts @@ -484,6 +484,56 @@ test.describe('Starter Templates from UI', () => { await expect(getResult().locator('text=You clicked')).toHaveText('You clicked 3 times.'); }); + test('nim Starter', async ({ page, getTestUrl }) => { + // the compiler is downloaded on the first run + test.slow(); + + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('[aria-label="Project"]'); + await app.click('text=New'); + await app.click('text=Nim Starter'); + + await waitForEditorFocus(app); + await waitForResultUpdate(); + + // the heading is set by the compiled program, and only then is the click handler wired up + await expect(getResult().locator('h1')).toHaveText('Hello, Nim!', { timeout: 120_000 }); + + await getResult().click('text=Click me'); + await getResult().click('text=Click me'); + await getResult().click('text=Click me'); + + await expect(getResult().locator('text=You clicked')).toHaveText('You clicked 3 times.'); + }); + + test('nim-wasm Starter', async ({ page, getTestUrl }) => { + // the compiler, linker and sysroot (~29 MB) are downloaded on the first run + test.slow(); + + await page.goto(getTestUrl()); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await app.click('[aria-label="Project"]'); + await app.click('text=New'); + await app.click('text=Nim (Wasm) Starter'); + + await waitForEditorFocus(app); + await waitForResultUpdate(); + + // the counter stays disabled until the compiler has loaded and run once + await expect(getResult().locator('#counter-button')).toBeEnabled({ timeout: 280_000 }); + + await getResult().click('text=Click me'); + await getResult().click('text=Click me'); + await getResult().click('text=Click me'); + + await expect(getResult().locator('h1')).toHaveText('Hello, Nim!'); + await expect(getResult().locator('text=You clicked')).toHaveText('You clicked 3 times.'); + }); test('haskell Starter', async ({ page, getTestUrl }) => { test.slow(); @@ -1228,6 +1278,48 @@ test.describe('Starter Templates from URL', () => { expect(counterText).toBe('You clicked 3 times.'); }); + test('nim Starter (in URL)', async ({ page, getTestUrl }) => { + // the compiler is downloaded on the first run + test.slow(); + + await page.goto(getTestUrl({ template: 'nim' })); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await waitForEditorFocus(app); + await waitForResultUpdate(); + + // the heading is set by the compiled program, and only then is the click handler wired up + await expect(getResult().locator('h1')).toHaveText('Hello, Nim!', { timeout: 120_000 }); + + await getResult().click('text=Click me'); + await getResult().click('text=Click me'); + await getResult().click('text=Click me'); + + await expect(getResult().locator('text=You clicked')).toHaveText('You clicked 3 times.'); + }); + + test('nim-wasm Starter (in URL)', async ({ page, getTestUrl }) => { + // the compiler, linker and sysroot (~29 MB) are downloaded on the first run + test.slow(); + + await page.goto(getTestUrl({ template: 'nim-wasm' })); + + const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); + + await waitForEditorFocus(app); + await waitForResultUpdate(); + + // the counter stays disabled until the compiler has loaded and run once + await expect(getResult().locator('#counter-button')).toBeEnabled({ timeout: 280_000 }); + + await getResult().click('text=Click me'); + await getResult().click('text=Click me'); + await getResult().click('text=Click me'); + + await expect(getResult().locator('h1')).toHaveText('Hello, Nim!'); + await expect(getResult().locator('text=You clicked')).toHaveText('You clicked 3 times.'); + }); test('haskell Starter (in URL)', async ({ page, getTestUrl }) => { test.slow(); diff --git a/functions/vendors/templates.js b/functions/vendors/templates.js index 25f0f93a75..9f4eaad59f 100644 --- a/functions/vendors/templates.js +++ b/functions/vendors/templates.js @@ -37,6 +37,8 @@ export const starterTemplates = { "rescript": "ReScript Starter", "reason": "Reason Starter", "ocaml": "Ocaml Starter", + "nim": "Nim Starter", + "nim-wasm": "Nim (Wasm) Starter", "python": "Python Starter", "python-wasm": "Python (Wasm) Starter", "r": "R Starter", diff --git a/scripts/build.js b/scripts/build.js index dfdac118b0..3764c3832f 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -228,6 +228,7 @@ const iifeBuild = () => 'languages/java/lang-java-script.ts', 'languages/cpp/lang-cpp-script.ts', 'languages/clang-wasm/lang-clang-wasm-script.ts', + 'languages/nim/lang-nim-wasm-script.ts', 'languages/fortran-wasm/lang-fortran-wasm-script.ts', 'languages/d-wasm/lang-d-wasm-script.ts', 'languages/cobol-wasm/lang-cobol-wasm-script.ts', diff --git a/server/php/inc/starter-templates.json b/server/php/inc/starter-templates.json index a1e2aa0659..068ab8c5fc 100644 --- a/server/php/inc/starter-templates.json +++ b/server/php/inc/starter-templates.json @@ -37,6 +37,8 @@ "rescript": "ReScript Starter", "reason": "Reason Starter", "ocaml": "Ocaml Starter", + "nim": "Nim Starter", + "nim-wasm": "Nim (Wasm) Starter", "python": "Python Starter", "python-wasm": "Python (Wasm) Starter", "r": "R Starter", diff --git a/src/livecodes/UI/command-menu-actions.ts b/src/livecodes/UI/command-menu-actions.ts index 0671054656..ca1c7e9bf9 100644 --- a/src/livecodes/UI/command-menu-actions.ts +++ b/src/livecodes/UI/command-menu-actions.ts @@ -293,6 +293,8 @@ export const getCommandMenuActions = ({ 'rescript', 'reason', 'ocaml', + 'nim', + 'nim-wasm', 'python', 'python-wasm', 'r', diff --git a/src/livecodes/assets/templates/nim.svg b/src/livecodes/assets/templates/nim.svg new file mode 100644 index 0000000000..12515ea9d9 --- /dev/null +++ b/src/livecodes/assets/templates/nim.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/livecodes/html/language-info.html b/src/livecodes/html/language-info.html index b654b1a206..6edda74fe2 100644 --- a/src/livecodes/html/language-info.html +++ b/src/livecodes/html/language-info.html @@ -1543,6 +1543,68 @@

Mustache

+
+

Nim

+
+ Nim compiled to JavaScript by the + Nim compiler, running + entirely in the browser. The program runs in the result page, so it can drive the DOM directly. +
+ +
+
+

Nim (Wasm)

+
+ Nim compiled to C and then to WebAssembly by + Clang, running entirely in + the browser. It supports standard input, command-line arguments and real exit codes. +
+ +

Nunjucks

diff --git a/src/livecodes/i18n/locales/en/language-info.lokalise.json b/src/livecodes/i18n/locales/en/language-info.lokalise.json index 9255770289..6636e67c74 100644 --- a/src/livecodes/i18n/locales/en/language-info.lokalise.json +++ b/src/livecodes/i18n/locales/en/language-info.lokalise.json @@ -712,6 +712,30 @@ "notes": "", "translation": "Mustache" }, + "nim.desc": { + "notes": "### ###\n\n\n", + "translation": "Nim compiled to JavaScript by the Nim compiler, running entirely in the browser. The program runs in the result page, so it can drive the DOM directly." + }, + "nim.link": { + "notes": "### ###\n
  • \n\n### ###\n\n\n### ###\n
  • \n\n### ###\n\n\n### ###\n
  • \n\n### ###\n\n\n### ###\n
  • \n\n### ###\n\n\n### ###\n
  • \n\n### ###\n\n\n", + "translation": " Nim official website Nim manual Nim standard library LiveCodes Documentation Load starter template " + }, + "nim.name": { + "notes": "", + "translation": "Nim" + }, + "nimWasm.desc": { + "notes": "### ###\n\n\n", + "translation": "Nim compiled to C and then to WebAssembly by Clang, running entirely in the browser. It supports standard input, command-line arguments and real exit codes." + }, + "nimWasm.link": { + "notes": "### ###\n
  • \n\n### ###\n\n\n### ###\n
  • \n\n### ###\n\n\n### ###\n
  • \n\n### ###\n\n\n### ###\n
  • \n\n### ###\n\n\n### ###\n
  • \n\n### ###\n\n\n", + "translation": " Nim official website Nim manual Nim standard library LiveCodes Documentation Load starter template " + }, + "nimWasm.name": { + "notes": "", + "translation": "Nim (Wasm)" + }, "nunjucks.desc": { "notes": "### ###\n\n\n", "translation": "A rich and powerful templating language for JavaScript. Nunjucks is essentially a port of jinja2" diff --git a/src/livecodes/i18n/locales/en/language-info.ts b/src/livecodes/i18n/locales/en/language-info.ts index e24ba7406f..ab41ff43d0 100644 --- a/src/livecodes/i18n/locales/en/language-info.ts +++ b/src/livecodes/i18n/locales/en/language-info.ts @@ -310,6 +310,16 @@ const languageInfo = { link: '<1> <2>Official website <3> <4>mustache(5) manual <5> <6>JavaScript implementation <7> <8>LiveCodes Documentations ', name: 'Mustache', }, + nim: { + desc: 'Nim compiled to JavaScript by the <1>Nim compiler, running entirely in the browser. The program runs in the result page, so it can drive the DOM directly.', + link: '<1> <2>Nim official website <3> <4>Nim manual <5> <6>Nim standard library <7> <8>LiveCodes Documentation <9> <10>Load starter template ', + name: 'Nim', + }, + nimWasm: { + desc: 'Nim compiled to C and then to WebAssembly by <1>Clang, running entirely in the browser. It supports standard input, command-line arguments and real exit codes.', + link: '<1> <2>Nim official website <3> <4>Nim manual <5> <6>Nim standard library <7> <8>LiveCodes Documentation <9> <10>Load starter template ', + name: 'Nim (Wasm)', + }, nunjucks: { desc: 'A rich and powerful templating language for JavaScript. Nunjucks is essentially a port of <1>jinja2', link: '<1> <2>Official website <3> <4>LiveCodes Documentations ', diff --git a/src/livecodes/i18n/locales/en/translation.lokalise.json b/src/livecodes/i18n/locales/en/translation.lokalise.json index 9699373161..d872d070f9 100644 --- a/src/livecodes/i18n/locales/en/translation.lokalise.json +++ b/src/livecodes/i18n/locales/en/translation.lokalise.json @@ -2760,6 +2760,14 @@ "notes": "", "translation": "MiniZinc Starter" }, + "templates.starter.nim": { + "notes": "", + "translation": "Nim Starter" + }, + "templates.starter.nim-wasm": { + "notes": "", + "translation": "Nim (Wasm) Starter" + }, "templates.starter.objc-wasm": { "notes": "", "translation": "Objective-C (Wasm) Starter" diff --git a/src/livecodes/i18n/locales/en/translation.ts b/src/livecodes/i18n/locales/en/translation.ts index 212c10234b..9c5f207032 100644 --- a/src/livecodes/i18n/locales/en/translation.ts +++ b/src/livecodes/i18n/locales/en/translation.ts @@ -1036,6 +1036,8 @@ const translation = { markdown: 'Markdown Starter', mdx: 'MDX Starter', minizinc: 'MiniZinc Starter', + nim: 'Nim Starter', + 'nim-wasm': 'Nim (Wasm) Starter', 'objc-wasm': 'Objective-C (Wasm) Starter', ocaml: 'Ocaml Starter', pascal: 'Pascal Starter', diff --git a/src/livecodes/languages/languages.ts b/src/livecodes/languages/languages.ts index bce826559c..8748664aae 100644 --- a/src/livecodes/languages/languages.ts +++ b/src/livecodes/languages/languages.ts @@ -57,6 +57,7 @@ import { mdx } from './mdx'; import { minizinc } from './minizinc'; import { mjml } from './mjml'; import { mustache } from './mustache'; +import { nim, nimWasm } from './nim'; import { nunjucks } from './nunjucks'; import { ocaml } from './ocaml'; import { pascal } from './pascal'; @@ -159,6 +160,8 @@ export const languages: LanguageSpecs[] = [ rescript, reason, ocaml, + nim, + nimWasm, python, pythonWasm, r, diff --git a/src/livecodes/languages/nim/__tests__/lang-nim-wasm.spec.ts b/src/livecodes/languages/nim/__tests__/lang-nim-wasm.spec.ts new file mode 100644 index 0000000000..17405bbc33 --- /dev/null +++ b/src/livecodes/languages/nim/__tests__/lang-nim-wasm.spec.ts @@ -0,0 +1,40 @@ +import type { Compiler, Config } from '../../../models'; +import { nimWasm } from '../lang-nim-wasm'; + +const getInlineScript = async (config: Config) => { + const inlineScript = (nimWasm.compiler as Compiler).inlineScript; + if (typeof inlineScript !== 'function') { + throw new Error('expected a function inlineScript for nim-wasm'); + } + return inlineScript({ baseUrl: '', config }); +}; + +const configWith = (customSettings: Config['customSettings']) => + ({ customSettings }) as unknown as Config; + +describe('nim-wasm custom settings', () => { + test('exposes an inlineScript', () => { + expect(typeof (nimWasm.compiler as Compiler).inlineScript).toBe('function'); + }); + + test('emits the settings under the language name', async () => { + const settings = { compileArgs: ['--define:release2'], args: ['--name', 'LiveCodes'] }; + const script = await getInlineScript(configWith({ 'nim-wasm': settings })); + expect(script).toContain('window.livecodes.nimWasm.settings'); + expect(script).toContain('"nim-wasm"'); + expect(script).toContain(JSON.stringify(settings)); + }); + + test('escapes a closing script tag in a setting', async () => { + const script = await getInlineScript( + configWith({ 'nim-wasm': { compileArgs: [''] } }), + ); + expect(script).not.toContain(''); + expect(script).toContain('\\u003c/script>'); + }); + + test('replaces previous settings so a removed setting does not linger', async () => { + const script = await getInlineScript(configWith({})); + expect(script).toContain('{ "nim-wasm": {} }'); + }); +}); diff --git a/src/livecodes/languages/nim/index.ts b/src/livecodes/languages/nim/index.ts new file mode 100644 index 0000000000..0181678430 --- /dev/null +++ b/src/livecodes/languages/nim/index.ts @@ -0,0 +1,2 @@ +export * from './lang-nim'; +export * from './lang-nim-wasm'; diff --git a/src/livecodes/languages/nim/lang-nim-wasm-script.ts b/src/livecodes/languages/nim/lang-nim-wasm-script.ts new file mode 100644 index 0000000000..3144a47071 --- /dev/null +++ b/src/livecodes/languages/nim/lang-nim-wasm-script.ts @@ -0,0 +1,177 @@ +/* eslint-disable no-console */ +import { clangWasmBaseUrl, nimWasmBaseUrl } from '../../vendors'; +import { createLoadingReporter, runCompiler } from '../wasm-runtime'; +import { createWorkerRunner, type Runner } from '../worker-runner'; + +// Nim's WebAssembly target: Nim compiles to C, and Clang and the linker turn that into a wasm +// module. Both block the thread they run on, so they run in a worker of their own — and the +// program runs there too, which is why stdin, stdout, stderr and the exit code all work. + +const SCRIPT_TYPE = 'text/nim-wasm'; + +/** Options forwarded to `@live-codes/nim-wasm`'s `run`, read from `config.customSettings`. */ +interface NimWasmSettings { + compileArgs?: string[]; + args?: string[]; +} + +interface RunResult { + input: string; + output: string | null; + error: string | null; + exitCode: number | null; +} + +interface NimWasmApi { + ready: boolean; + input: string; + output: string | null; + error: string | null; + exitCode: number | null; + loaded: Promise; + init: Promise | null; + runner?: Runner; + settings?: Record; + run: (input?: string) => Promise; +} + +declare const window: Window & { + livecodes: Record; +}; + +const getWorkerSrc = () => ` +importScripts(${JSON.stringify(clangWasmBaseUrl + 'dist/clang-wasm-toolchain.global.js')}); +importScripts(${JSON.stringify(nimWasmBaseUrl + 'dist/nim-wasm.global.js')}); + +let compiler = null; + +const getCompiler = () => { + compiler = + compiler || + (async () => { + // Compile through the Clang runtime this page already uses, rather than the second copy the + // Nim IIFE carries inside it, so only one runtime is ever loaded. + const toolchain = await self.clangWasmToolchain.createToolchain({ + baseUrl: ${JSON.stringify(clangWasmBaseUrl + 'assets/')}, + }); + return self.nimWasm.createCompiler({ + target: 'wasm', + baseUrl: ${JSON.stringify(nimWasmBaseUrl + 'assets/nim/')}, + toolchain, + }); + })(); + return compiler; +}; + +addEventListener('message', async (event) => { + const { id, code, input, options } = event.data; + try { + const result = await (await getCompiler()).run(code, input, options); + postMessage({ + id, + result: { + output: result.output, + errors: result.errors, + exitCode: result.exitCode, + }, + }); + } catch (error) { + postMessage({ id, error: String((error && error.message) || error) }); + } +}); + +getCompiler().then( + () => postMessage({ type: 'ready' }), + (error) => postMessage({ type: 'error', message: String((error && error.message) || error) }), +); +`; + +const reportLoading = createLoadingReporter(); + +const setResult = ( + input: string, + output: string | null, + error: string | null, + exitCode: number | null, +): RunResult => { + nimWasm.input = input; + nimWasm.output = output; + nimWasm.error = error; + nimWasm.exitCode = exitCode; + nimWasm.ready = true; + + if (error != null) { + console.error(error); + } else if (output != null) { + console.log(output); + } + return { input, output, error, exitCode }; +}; + +window.livecodes.nimWasm ??= {} as NimWasmApi; + +const nimWasm = window.livecodes.nimWasm; +nimWasm.ready = false; + +/** The custom settings of the running language, injected by the language's `inlineScript`. */ +const getSettings = (): NimWasmSettings => { + const settings = nimWasm.settings?.['nim-wasm']; + return settings != null && typeof settings === 'object' ? settings : {}; +}; + +const getCode = () => { + let code = ''; + document.querySelectorAll(`script[type="${SCRIPT_TYPE}"]`).forEach((script) => { + code += `${script.innerHTML}\n`; + }); + return code; +}; + +/** Start (once) downloading the toolchain, showing the loading indicator while it happens. */ +const ensureLoaded = (runner: Runner): Promise => { + let init = nimWasm.init; + if (!init) { + init = (async () => { + reportLoading(true); + try { + await runner.ensureReady(); + } finally { + reportLoading(false); + } + })().catch((error: Error) => { + // Reset so a later run can retry the download. + nimWasm.init = null; + throw error; + }); + // The failure is surfaced through `run`; do not also report it unhandled. + init.catch(() => undefined); + nimWasm.init = init; + } + return init; +}; + +nimWasm.loaded = new Promise((resolve) => { + const interval = setInterval(() => { + if (nimWasm.ready) { + clearInterval(interval); + resolve(); + } + }, 50); +}); + +nimWasm.run = async (input?: string) => { + const stdin = `${input ?? nimWasm.input ?? ''}`; + const code = getCode(); + if (!code.trim()) return setResult(stdin, null, null, null); + + const runner = (nimWasm.runner = + nimWasm.runner || createWorkerRunner({ getWorkerSrc, label: 'Nim' })); + + return runCompiler(runner, ensureLoaded, code, stdin, setResult, getSettings()); +}; + +window.addEventListener('load', async () => { + reportLoading(true); + await nimWasm.run(nimWasm.input); + reportLoading(false); +}); diff --git a/src/livecodes/languages/nim/lang-nim-wasm.ts b/src/livecodes/languages/nim/lang-nim-wasm.ts new file mode 100644 index 0000000000..70e4b0f000 --- /dev/null +++ b/src/livecodes/languages/nim/lang-nim-wasm.ts @@ -0,0 +1,32 @@ +import type { Config, LanguageSpecs } from '../../models'; +import { getLanguageCustomSettings } from '../../utils'; +import { nimEditorSupport } from './nim-editor'; + +// Nim's WebAssembly target compiles to C and then through the Clang toolchain, so it runs like the +// Clang languages: the compiler here is a pass-through and the source reaches the result page as a +// `text/nim-wasm` tag, where `lang-nim-wasm-script.ts` compiles and runs it in a worker. +const getSettingsScript = (config: Config) => { + const settings = getLanguageCustomSettings('nim-wasm', config); + // `\u003c` keeps a `` in a setting (e.g. a compile argument) from closing the tag. + const serialized = JSON.stringify(settings).replace(/ async (code) => code, + scripts: ({ baseUrl }) => [baseUrl + '{{hash:lang-nim-wasm-script.js}}'], + inlineScript: ({ config }) => getSettingsScript(config), + scriptType: 'text/nim-wasm', + compiledCodeLanguage: 'nim-wasm', + liveReload: true, + }, + extensions: ['nimwasm', 'nim-wasm', 'wasm.nim'], + editor: 'script', + editorSupport: nimEditorSupport, + largeDownload: true, +}; diff --git a/src/livecodes/languages/nim/lang-nim.ts b/src/livecodes/languages/nim/lang-nim.ts new file mode 100644 index 0000000000..aaed030f3d --- /dev/null +++ b/src/livecodes/languages/nim/lang-nim.ts @@ -0,0 +1,40 @@ +import type { CompileResult, LanguageSpecs } from '../../models'; +import { nimWasmBaseUrl } from '../../vendors'; +import { nimEditorSupport } from './nim-editor'; + +// Nim's JavaScript backend, for pages that want a document: the compiler emits one self-sufficient +// file, so it is compiled with `execute: false` and the result is run in the result page. That is +// the shape the TypeScript compiler's factory has; `compiledCodeLanguage` says the output is JS. +export const nim: LanguageSpecs = { + name: 'nim', + title: 'Nim', + compiler: { + url: nimWasmBaseUrl + 'dist/nim-wasm.global.js', + factory: () => { + let compiler: Promise | null = null; + const getCompiler = () => { + compiler = + compiler || + (self as any).nimWasm.createCompiler({ + target: 'js', + baseUrl: nimWasmBaseUrl + 'assets/nim/', + }); + return compiler; + }; + return async (code: string): Promise => { + const result = await (await getCompiler()).run(code, '', { execute: false }); + if (!result.compiledCode) { + return { code: '', info: { errors: result.errors || ['Nim produced no output.'] } }; + } + // The emitted program declares its runtime at the top level and calls `main()` at the + // end, so it gets a scope of its own rather than sharing the page's globals. + return { code: `(() => {\n${result.compiledCode}\n})();`, info: {} }; + }; + }, + compiledCodeLanguage: 'javascript', + liveReload: true, + }, + extensions: ['nim', 'nims', 'nimble'], + editor: 'script', + editorSupport: nimEditorSupport, +}; diff --git a/src/livecodes/languages/nim/nim-editor.ts b/src/livecodes/languages/nim/nim-editor.ts new file mode 100644 index 0000000000..c3b2664b20 --- /dev/null +++ b/src/livecodes/languages/nim/nim-editor.ts @@ -0,0 +1,47 @@ +import { codemirrorLegacy } from '../../editor/codemirror/utils'; +import type { LanguageSpecs } from '../../models'; +import { codeMirrorBaseUrl, monacoLanguagesBaseUrl } from '../../vendors'; + +const words = (list: string): Record => + Object.fromEntries(list.split(' ').map((word) => [word, true])); + +// The shared CodeMirror build ships no Nim mode, so Nim is described through the `clike` factory +// it does ship: the highlighting is approximate (Nim is indented, not braced), but the keyword +// and comment sets are Nim's, which is what the mode is judged on. +const nimCodemirror = async () => { + const { clike } = await import(codeMirrorBaseUrl + 'codemirror-lang-clike.js'); + return codemirrorLegacy( + clike({ + name: 'nim', + keywords: words( + 'addr and as asm bind block break case cast concept const continue converter defer ' + + 'discard distinct div do elif else end enum except export finally for from func if ' + + 'import in include interface is isnot iterator let macro method mixin mod nil not ' + + 'notin object of or out proc ptr raise ref return shl shr static template try tuple ' + + 'type using var when while xor yield', + ), + types: words( + 'bool char float int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 string ' + + 'cstring pointer seq array openarray varargs range', + ), + builtin: words('echo len add new setLen ord chr high low inc dec repr quit assert doAssert'), + blockKeywords: words('case do elif else except finally for if of try when while'), + defKeywords: words('func iterator macro method proc template type converter'), + atoms: words('true false nil'), + hooks: { + // Nim comments start with `#`, unlike the `//` the C-like modes expect. + '#': (stream: any) => { + stream.skipToEnd(); + return 'comment'; + }, + }, + languageData: { commentTokens: { line: '#' } }, + }), + ); +}; + +export const nimEditorSupport: NonNullable = { + monaco: { languageSupport: monacoLanguagesBaseUrl + 'nim.js', language: 'nim' }, + codemirror: { languageSupport: nimCodemirror }, + codejar: { language: 'nim' }, +}; diff --git a/src/livecodes/models.ts b/src/livecodes/models.ts index 0db6ff321b..0e94779466 100644 --- a/src/livecodes/models.ts +++ b/src/livecodes/models.ts @@ -201,6 +201,7 @@ export interface Compiler { | 'text/cpp-wasm' | 'text/objc-wasm' | 'text/objcpp-wasm' + | 'text/nim-wasm' | 'text/fortran-wasm' | 'text/d-wasm' | 'text/cobol-wasm' diff --git a/src/livecodes/templates/starter/index.ts b/src/livecodes/templates/starter/index.ts index 76cc140576..90663fce2c 100644 --- a/src/livecodes/templates/starter/index.ts +++ b/src/livecodes/templates/starter/index.ts @@ -51,6 +51,8 @@ import { malinaStarter } from './malina-starter'; import { markdownStarter } from './markdown-starter'; import { mdxStarter } from './mdx-starter'; import { minizincStarter } from './minizinc-starter'; +import { nimStarter } from './nim-starter'; +import { nimWasmStarter } from './nim-wasm-starter'; import { ocamlStarter } from './ocaml-starter'; import { pascalStarter } from './pascal-starter'; import { perlStarter } from './perl-starter'; @@ -128,6 +130,8 @@ export const starterTemplates = [ rescriptStarter, reasonStarter, ocamlStarter, + nimStarter, + nimWasmStarter, pythonStarter, pythonWasmStarter, rStarter, diff --git a/src/livecodes/templates/starter/nim-starter.ts b/src/livecodes/templates/starter/nim-starter.ts new file mode 100644 index 0000000000..04b9d11d2e --- /dev/null +++ b/src/livecodes/templates/starter/nim-starter.ts @@ -0,0 +1,52 @@ +import type { Template } from '../../models'; + +export const nimStarter: Template = { + name: 'nim', + title: window.deps.translateString('templates.starter.nim', 'Nim Starter'), + thumbnail: 'assets/templates/nim.svg', + activeEditor: 'script', + markup: { + language: 'html', + content: ` +
    +

    Hello, World!

    + +

    You clicked 0 times.

    + +
    +`.trimStart(), + }, + style: { + language: 'css', + content: ` +.container, +.container button { + text-align: center; + font: 1em sans-serif; +} +.logo { + width: 150px; +} +`.trimStart(), + }, + script: { + language: 'nim', + // The JavaScript backend runs the program in the result page, so it can drive the DOM directly. + content: ` +import dom + +var count = 0 + +proc render() = + document.getElementById("name").innerText = "Nim" + document.getElementById("counter").innerText = $count + +proc onClick(event: Event) = + inc count + render() + +document.getElementById("counter-button").addEventListener("click", onClick) +render() +`.trimStart(), + }, +}; diff --git a/src/livecodes/templates/starter/nim-wasm-starter.ts b/src/livecodes/templates/starter/nim-wasm-starter.ts new file mode 100644 index 0000000000..bef4ab6896 --- /dev/null +++ b/src/livecodes/templates/starter/nim-wasm-starter.ts @@ -0,0 +1,78 @@ +import type { Template } from '../../models'; + +export const nimWasmStarter: Template = { + name: 'nim-wasm', + title: window.deps.translateString('templates.starter.nim-wasm', 'Nim (Wasm) Starter'), + thumbnail: 'assets/templates/nim.svg', + activeEditor: 'script', + markup: { + language: 'html', + content: ` +
    +

    Hello, World!

    + +

    You clicked 0 times.

    + +
    + + +`.trimStart(), + }, + style: { + language: 'css', + content: ` +.container, +.container button { + text-align: center; + font: 1em sans-serif; +} +.logo { + width: 150px; +} +`.trimStart(), + }, + script: { + language: 'nim-wasm', + // Compiled to C and then to WebAssembly by the Clang toolchain, which the runner drives in its own worker. + content: ` +import std/strutils + +echo "Nim" +let count = stdin.readLine().parseInt() +echo count + 1 +`.trimStart(), + }, +}; diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts index 18e1005509..b19681d518 100644 --- a/src/livecodes/vendors.ts +++ b/src/livecodes/vendors.ts @@ -375,6 +375,8 @@ export const monacoVolarUrl = /* @__PURE__ */ getUrl( export const mustacheUrl = /* @__PURE__ */ getUrl('mustache@4.2.0/mustache.js'); +export const nimWasmBaseUrl = /* @__PURE__ */ getUrl('@live-codes/nim-wasm@0.4.0/'); + export const ninjaKeysUrl = /* @__PURE__ */ getUrl('@hatemhosny/ninja-keys@1.14.0/bundle/index.js'); export const nomnomlCdnUrl = /* @__PURE__ */ getUrl('nomnoml@1.6.1/dist/nomnoml.js'); diff --git a/src/sdk/models.ts b/src/sdk/models.ts index e1c41d24e1..6d494dcdb8 100644 --- a/src/sdk/models.ts +++ b/src/sdk/models.ts @@ -313,6 +313,12 @@ export type Language = | 'ocaml' | 'ml' | 'mli' + | 'nim' + | 'nims' + | 'nimble' + | 'nim-wasm' + | 'nimwasm' + | 'wasm.nim' | 'tcl' | 'wat' | 'wast' @@ -487,6 +493,8 @@ export type TemplateName = | 'rescript' | 'reason' | 'ocaml' + | 'nim' + | 'nim-wasm' | 'python' | 'python-wasm' | 'r' @@ -915,6 +923,7 @@ export type CustomSettings = Partial< | 'text/cpp-wasm' | 'text/objc-wasm' | 'text/objcpp-wasm' + | 'text/nim-wasm' | 'text/java' | 'text/csharp-wasm' | 'text/razor' diff --git a/storybook/_stories/EmbedOptions/template.ts b/storybook/_stories/EmbedOptions/template.ts index 199c3c74c2..6c61101a0f 100644 --- a/storybook/_stories/EmbedOptions/template.ts +++ b/storybook/_stories/EmbedOptions/template.ts @@ -39,6 +39,8 @@ const storyDef: StoryDef = { ReScript: { props: { template: 'rescript' }, storyName: 'ReScript' }, Reason: { props: { template: 'reason' } }, Ocaml: { props: { template: 'ocaml' } }, + Nim: { props: { template: 'nim' } }, + NimWasm: { props: { template: 'nim-wasm' } }, Python: { props: { template: 'python' } }, PythonWasm: { props: { template: 'python-wasm' } }, R: { props: { template: 'r' } }, diff --git a/vendor-licenses.md b/vendor-licenses.md index 0af0caf3bf..c6d56d4214 100644 --- a/vendor-licenses.md +++ b/vendor-licenses.md @@ -192,6 +192,8 @@ Monaspace font: [OFL-1.1 License](https://github.com/githubnext/monaspace/blob/3 mustache.js: [MIT License](https://github.com/janl/mustache.js/blob/972fd2b27a036888acfcb60d6119317744fac7ee/LICENSE) +Nim: [MIT License](https://github.com/nim-lang/Nim/blob/version-2-2/COPYING) (the Nim compiler, compiled to WebAssembly and packaged by [@live-codes/nim-wasm](https://www.npmjs.com/package/@live-codes/nim-wasm): [MIT License](https://github.com/live-codes/browser-nim/blob/main/LICENSE)) + nomnoml: [MIT License](https://github.com/skanaar/nomnoml/blob/87776185c2c57047e8e0bfc549d1c7093844daee/LICENSE) normalize.css: [MIT License](https://github.com/necolas/normalize.css/blob/fc091cce1534909334c1911709a39c22d406977b/LICENSE.md) From 3f214ab61ed0fdcc73c34cc8b8170d8b8248bca7 Mon Sep 17 00:00:00 2001 From: "pullfrog[bot]" <226033991+pullfrog[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:39:36 +0000 Subject: [PATCH 2/7] fix(Compilers): thread Nim wasm compileArgs and handle boot failures --- .../languages/nim/lang-nim-wasm-script.ts | 70 +++++++++++++------ src/livecodes/languages/nim/lang-nim.ts | 11 ++- .../templates/starter/nim-wasm-starter.ts | 12 +++- 3 files changed, 68 insertions(+), 25 deletions(-) diff --git a/src/livecodes/languages/nim/lang-nim-wasm-script.ts b/src/livecodes/languages/nim/lang-nim-wasm-script.ts index 3144a47071..1b3d52e977 100644 --- a/src/livecodes/languages/nim/lang-nim-wasm-script.ts +++ b/src/livecodes/languages/nim/lang-nim-wasm-script.ts @@ -9,7 +9,10 @@ import { createWorkerRunner, type Runner } from '../worker-runner'; const SCRIPT_TYPE = 'text/nim-wasm'; -/** Options forwarded to `@live-codes/nim-wasm`'s `run`, read from `config.customSettings`. */ +/** + * Options read from `config.customSettings`: `compileArgs` is fixed when the compiler is created, + * while `args` is a per-run option. + */ interface NimWasmSettings { compileArgs?: string[]; args?: string[]; @@ -24,6 +27,7 @@ interface RunResult { interface NimWasmApi { ready: boolean; + failed: boolean; input: string; output: string | null; error: string | null; @@ -43,30 +47,50 @@ const getWorkerSrc = () => ` importScripts(${JSON.stringify(clangWasmBaseUrl + 'dist/clang-wasm-toolchain.global.js')}); importScripts(${JSON.stringify(nimWasmBaseUrl + 'dist/nim-wasm.global.js')}); +let toolchain = null; let compiler = null; +let compilerArgs = null; -const getCompiler = () => { - compiler = - compiler || - (async () => { - // Compile through the Clang runtime this page already uses, rather than the second copy the - // Nim IIFE carries inside it, so only one runtime is ever loaded. - const toolchain = await self.clangWasmToolchain.createToolchain({ - baseUrl: ${JSON.stringify(clangWasmBaseUrl + 'assets/')}, - }); - return self.nimWasm.createCompiler({ - target: 'wasm', - baseUrl: ${JSON.stringify(nimWasmBaseUrl + 'assets/nim/')}, - toolchain, - }); - })(); +const getToolchain = () => { + toolchain = + toolchain || + self.clangWasmToolchain.createToolchain({ + baseUrl: ${JSON.stringify(clangWasmBaseUrl + 'assets/')}, + }); + return toolchain; +}; + +const getCompiler = (compileArgs = []) => { + const key = JSON.stringify(compileArgs); + if (compiler && compilerArgs === key) return compiler; + compilerArgs = key; + const previous = compiler; + compiler = (async () => { + // Compile through the Clang runtime this page already uses, rather than the second copy the + // Nim IIFE carries inside it, so only one runtime is ever loaded. + const built = await getToolchain(); + const next = await self.nimWasm.createCompiler({ + target: 'wasm', + baseUrl: ${JSON.stringify(nimWasmBaseUrl + 'assets/nim/')}, + toolchain: built, + compileArgs, + }); + // compileArgs is fixed when a compiler is created, so a change builds a new one and + // releases the old; the shared toolchain belongs to this page and is not released. + if (previous) previous.then((old) => old.dispose()).catch(() => undefined); + return next; + })(); return compiler; }; addEventListener('message', async (event) => { const { id, code, input, options } = event.data; try { - const result = await (await getCompiler()).run(code, input, options); + const result = await (await getCompiler(options && options.compileArgs)).run( + code, + input, + options, + ); postMessage({ id, result: { @@ -112,6 +136,7 @@ window.livecodes.nimWasm ??= {} as NimWasmApi; const nimWasm = window.livecodes.nimWasm; nimWasm.ready = false; +nimWasm.failed = false; /** The custom settings of the running language, injected by the language's `inlineScript`. */ const getSettings = (): NimWasmSettings => { @@ -131,6 +156,7 @@ const getCode = () => { const ensureLoaded = (runner: Runner): Promise => { let init = nimWasm.init; if (!init) { + nimWasm.failed = false; init = (async () => { reportLoading(true); try { @@ -139,8 +165,9 @@ const ensureLoaded = (runner: Runner): Promise => { reportLoading(false); } })().catch((error: Error) => { - // Reset so a later run can retry the download. + // Reset so a later run can retry the download, and let `loaded` reject. nimWasm.init = null; + nimWasm.failed = true; throw error; }); // The failure is surfaced through `run`; do not also report it unhandled. @@ -150,9 +177,12 @@ const ensureLoaded = (runner: Runner): Promise => { return init; }; -nimWasm.loaded = new Promise((resolve) => { +nimWasm.loaded = new Promise((resolve, reject) => { const interval = setInterval(() => { - if (nimWasm.ready) { + if (nimWasm.failed) { + clearInterval(interval); + reject(new Error(nimWasm.error || 'Failed to initialize the Nim compiler')); + } else if (nimWasm.ready) { clearInterval(interval); resolve(); } diff --git a/src/livecodes/languages/nim/lang-nim.ts b/src/livecodes/languages/nim/lang-nim.ts index aaed030f3d..2be877d6f5 100644 --- a/src/livecodes/languages/nim/lang-nim.ts +++ b/src/livecodes/languages/nim/lang-nim.ts @@ -13,12 +13,17 @@ export const nim: LanguageSpecs = { factory: () => { let compiler: Promise | null = null; const getCompiler = () => { - compiler = - compiler || - (self as any).nimWasm.createCompiler({ + if (!compiler) { + const created: Promise = (self as any).nimWasm.createCompiler({ target: 'js', baseUrl: nimWasmBaseUrl + 'assets/nim/', }); + // do not keep a failed boot (e.g. a transient network error) + created.catch(() => { + compiler = null; + }); + compiler = created; + } return compiler; }; return async (code: string): Promise => { diff --git a/src/livecodes/templates/starter/nim-wasm-starter.ts b/src/livecodes/templates/starter/nim-wasm-starter.ts index bef4ab6896..9fe05df9e7 100644 --- a/src/livecodes/templates/starter/nim-wasm-starter.ts +++ b/src/livecodes/templates/starter/nim-wasm-starter.ts @@ -22,7 +22,15 @@ export const nimWasmStarter: Template = { addEventListener('load', async () => { const button = document.querySelector("#counter-button"); - await livecodes.nimWasm.loaded; + try { + await livecodes.nimWasm.loaded; + } catch (error) { + // surface the boot error instead of crashing on a null output + button.innerText = "Error"; + console.error(error); + return; + } + update(livecodes.nimWasm.output); button.onclick = async () => { @@ -35,7 +43,7 @@ export const nimWasmStarter: Template = { const counter = document.querySelector("#counter"); const name = document.querySelector("#name"); - const [title, count] = output.split('\\n'); + const [title, count] = (output || '').split('\\n'); if (!isNaN(Number(count))) { window.count = count; From a433037d2bb743d700c9e5513ea5cb8d9fb56d37 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Wed, 7 Oct 2026 13:06:41 +0300 Subject: [PATCH 3/7] nim fixes --- docs/docs/languages/nim-wasm.mdx | 2 + docs/docs/languages/nim.mdx | 10 +- src/livecodes/html/language-info.html | 106 ++++++++++-------- .../locales/en/language-info.lokalise.json | 104 ++++++++--------- .../i18n/locales/en/language-info.ts | 6 +- .../languages/nim/lang-nim-wasm-script.ts | 9 +- src/livecodes/languages/nim/lang-nim-wasm.ts | 13 ++- src/livecodes/languages/nim/lang-nim.ts | 15 ++- src/livecodes/languages/nim/nim-editor.ts | 47 -------- src/livecodes/languages/v-wasm/lang-v-wasm.ts | 2 +- src/livecodes/vendors.ts | 2 +- src/sdk/models.ts | 2 + 12 files changed, 150 insertions(+), 168 deletions(-) delete mode 100644 src/livecodes/languages/nim/nim-editor.ts diff --git a/docs/docs/languages/nim-wasm.mdx b/docs/docs/languages/nim-wasm.mdx index 2c296c67fb..ff3e792cbf 100644 --- a/docs/docs/languages/nim-wasm.mdx +++ b/docs/docs/languages/nim-wasm.mdx @@ -4,6 +4,8 @@ In LiveCodes, Nim runs in the browser using the [Nim compiler](https://nim-lang.org/) compiled to WebAssembly. This target (Nim's `c` backend) compiles the program to C and then to WebAssembly using the [Clang](https://clang.llvm.org/) toolchain, the same one used for [C (Wasm)](./c-wasm.mdx), entirely client-side. +> For Nim's JavaScript backend, which compiles to JavaScript and allows DOM access for the [result page](../features/result.mdx), see [Nim](./nim.mdx). + ## Usage Demo: diff --git a/docs/docs/languages/nim.mdx b/docs/docs/languages/nim.mdx index 57894e8dd7..a718391885 100644 --- a/docs/docs/languages/nim.mdx +++ b/docs/docs/languages/nim.mdx @@ -31,7 +31,7 @@ export const nimConfig = { Nim's JavaScript backend emits one self-sufficient program that runs in the result page. It has a document and a console, so `echo` writes to the console, and the browser modules that ship with the compiler (`dom`, `jsffi`, `jsconsole`, `jscore`, `jsre` and `asyncjs`) are on its search path: -```nim +```js import dom document.title = "Nim in LiveCodes" @@ -42,7 +42,7 @@ document.body.appendChild(element) Example: - + ## Language Info @@ -66,13 +66,9 @@ The [Nim compiler](https://nim-lang.org/) (Nim's `js` backend) compiled to WebAs Nim `v2.2.4`. -## Live Reload - -By default, new code changes are sent to the result page without a full page reload, avoiding the need to re-download the compiler. Adding the code comment `# __livecodes_reload__` forces a full page reload. - ## Example Usage -```nim +```js import std/strutils echo "Hello, World!" diff --git a/src/livecodes/html/language-info.html b/src/livecodes/html/language-info.html index 6edda74fe2..c89b361b06 100644 --- a/src/livecodes/html/language-info.html +++ b/src/livecodes/html/language-info.html @@ -67,7 +67,7 @@

    AsciiDoc

    >
  • - Learn X in Y minutes, where X=asciidoc
  • @@ -285,7 +285,7 @@

    C (Wasm)

    Clang official website
  • - Learn X in Y minutes, where X=C
  • @@ -361,7 +361,7 @@

    ClojureScript (Cherry)

    Cherry repo
  • - Learn X in Y minutes, where X=clojure
  • @@ -403,7 +403,7 @@

    CLJS (self-hosted)

    >
  • - Learn X in Y minutes, where X=clojure
  • @@ -437,7 +437,7 @@

    COBOL (Wasm)

    >
  • - Learn X in Y minutes, where X=COBOL
  • @@ -463,7 +463,7 @@

    CoffeeScript

    >
  • - Learn X in Y minutes, where X=coffeescript
  • @@ -493,7 +493,7 @@

    Common Lisp

    >
  • - Learn X in Y minutes, where X=Common Lisp
  • @@ -524,7 +524,7 @@

    C++

    JSCPP
  • - Learn X in Y minutes, where X=C++
  • @@ -552,7 +552,7 @@

    C++ (Wasm)

    Clang official website
  • - Learn X in Y minutes, where X=C++
  • @@ -586,7 +586,7 @@

    C# (Wasm)

    >
  • - Learn X in Y minutes, where X=C#
  • @@ -622,7 +622,7 @@

    D (Wasm)

    D language tour
  • - Learn X in Y minutes, where X=D
  • @@ -983,7 +983,7 @@

    Go

    >
  • - Learn X in Y minutes, where X=Go
  • @@ -1008,7 +1008,7 @@

    Go (Wasm)

  • Go official website
  • Yaegi
  • - Learn X in Y minutes, where X=Go
  • @@ -1040,7 +1040,7 @@

    Haml

    >
  • - Learn X in Y minutes, where X=haml
  • @@ -1143,7 +1143,7 @@

    Java

    DoppioJVM
  • - Learn X in Y minutes, where X=java
  • @@ -1174,7 +1174,7 @@

    JavaScript

    >
  • - Learn X in Y minutes, where X=JavaScript
  • @@ -1269,7 +1269,7 @@

    Julia

    >
  • - Learn X in Y minutes, where X=Julia
  • @@ -1286,7 +1286,7 @@

    Less