diff --git a/docs/docs/languages/nim.mdx b/docs/docs/languages/nim.mdx
index 93f59fdb7f..627d82ea62 100644
--- a/docs/docs/languages/nim.mdx
+++ b/docs/docs/languages/nim.mdx
@@ -2,13 +2,16 @@
[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).
+In LiveCodes, Nim runs in the browser using the [Nim compiler](https://nim-lang.org/) compiled to WebAssembly. The `nim` language drives any of Nim's backends, selected with the `target` [custom setting](#custom-settings):
-> For Nim's WebAssembly backend, which supports standard input, command-line arguments and real exit codes, see [Nim (Wasm)](./nim-wasm.mdx).
+- **`js`** (default) — Nim's JavaScript backend. The emitted program runs in the result page, so it can drive the DOM directly (e.g. through the `dom` and `jsffi` modules).
+- **`c`** and **`cpp`** — Nim's C and C++ backends. The generated source is compiled and run in a worker by the [Clang](https://clang.llvm.org/) toolchain, the same one used for [C (Wasm)](./c-wasm.mdx), and the C/C++ that Nim generated is shown in the compiled code view.
+
+> For Nim's `c` backend as a dedicated language, with standard input, command-line arguments and real exit codes, see [Nim (Wasm)](./nim-wasm.mdx).
## Usage
-Demo:
+Demo (JavaScript backend):
import LiveCodes from '../../src/components/LiveCodes.tsx';
@@ -27,9 +30,25 @@ export const nimConfig = {
+### Choosing a backend
+
+Set `target` in [custom settings](../advanced/custom-settings.mdx):
+
+```json title="Custom Settings"
+{
+ "nim": {
+ "target": "cpp"
+ }
+}
+```
+
+The `c` and `cpp` targets compile and run in a worker, like [C (Wasm)](./c-wasm.mdx), so they have no document. They do have C interop, exact 64-bit integers, `cast`, pointers and `sizeof` — the capabilities the JavaScript backend lacks. Standard input and command-line arguments are not exposed by this language; use [Nim (Wasm)](./nim-wasm.mdx) for those.
+
+With either target, the **compiled** code view shows what Nim emitted: the JavaScript program, or the C/C++ that was handed to Clang.
+
### 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:
+With the default `js` backend, Nim 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
@@ -60,11 +79,27 @@ Example:
## 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.
+The [Nim compiler](https://nim-lang.org/) (Nim's `js`, `c` or `cpp` backend) compiled to WebAssembly, packaged as [@live-codes/nim-wasm](https://www.npmjs.com/package/@live-codes/nim-wasm). The `c` and `cpp` targets emit one self-contained source string, which the [Clang](https://clang.llvm.org/) toolchain of [@live-codes/clang-wasm](https://www.npmjs.com/package/@live-codes/clang-wasm) compiles and links.
### Version
-Nim `v2.2.4`.
+Nim `v2.2.4`. Clang `v22.1.8` for the `c` and `cpp` targets.
+
+## Custom Settings
+
+The compiler can be configured using [custom settings](../advanced/custom-settings.mdx) added to the property `nim`:
+
+| Property | Type | Description |
+| --- | --- | --- |
+| `target` | `"js" \| "c" \| "cpp"` | The Nim backend. Defaults to `js`. |
+
+```json title="Custom Settings"
+{
+ "nim": {
+ "target": "c"
+ }
+}
+```
## Example Usage
@@ -86,3 +121,5 @@ https://livecodes.io/?template=nim
- [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
+- [C (Wasm)](./c-wasm.mdx) in LiveCodes
+- [C++ (Wasm)](./cpp-wasm.mdx) in LiveCodes
diff --git a/src/livecodes/UI/deploy.ts b/src/livecodes/UI/deploy.ts
index ebd774f130..147f208d21 100644
--- a/src/livecodes/UI/deploy.ts
+++ b/src/livecodes/UI/deploy.ts
@@ -116,7 +116,10 @@ export const createDeployUI = async ({
newRepo: boolean,
) => {
const forExport = true;
- const scriptType = deps.getLanguageCompiler(deps.getConfig().script.language)?.scriptType;
+ let scriptType = deps.getLanguageCompiler(deps.getConfig().script.language)?.scriptType;
+ if (typeof scriptType === 'function') {
+ scriptType = scriptType({ config: deps.getConfig() });
+ }
const singleFile = scriptType != null && scriptType !== 'module';
newRepoNameError.innerHTML = '';
diff --git a/src/livecodes/core.ts b/src/livecodes/core.ts
index a682ef2d9c..9f9746bd6d 100644
--- a/src/livecodes/core.ts
+++ b/src/livecodes/core.ts
@@ -897,6 +897,13 @@ const registerRun = (editorId: EditorId, editors: Editors) => {
});
};
+const getCompiledCodeLanguage = (lang: Language, config: Config) => {
+ const compiledLanguage = getLanguageCompiler(lang)?.compiledCodeLanguage;
+ if (typeof compiledLanguage === 'string') return compiledLanguage;
+ if (!compiledLanguage) return null;
+ return compiledLanguage({ config });
+};
+
const updateCompiledCode = () => {
const getCompiledLanguage = (editorId: EditorId) => {
const defaultLang: { [key in EditorId]: Language } = {
@@ -904,10 +911,12 @@ const updateCompiledCode = () => {
style: 'css',
script: 'javascript',
};
- const lang = getLanguageCompiler(getConfig()[editorId].language)?.compiledCodeLanguage;
+ const config = getConfig();
+ const lang = getCompiledCodeLanguage(config[editorId].language, config);
return {
language: lang || defaultLang[editorId],
- label: lang === 'json' ? 'JSON' : getLanguageByAlias(lang) || lang || defaultLang[editorId],
+ label:
+ lang === 'json' ? 'JSON' : getLanguageByAlias(lang || '') || lang || defaultLang[editorId],
};
};
const compiledLanguages: { [key in EditorId]: { language: Language; label: string } } = {
@@ -963,7 +972,10 @@ const getResultPage = async ({
const styleLanguage = config.style.language;
const scriptLanguage = config.script.language;
const testsLanguage = config.tests?.language || 'typescript';
- const scriptType = getLanguageCompiler(scriptLanguage)?.scriptType;
+ let scriptType = getLanguageCompiler(scriptLanguage)?.scriptType;
+ if (typeof scriptType === 'function') {
+ scriptType = scriptType({ config });
+ }
const forceCompileStyles =
[...config.processors, ...getCache().processors].find((name) =>
@@ -1154,10 +1166,11 @@ const flushResult = () => {
iframe.contentWindow.postMessage({ flush: true }, '*');
+ const config = getConfig();
const compiledLanguages = {
- markup: getLanguageCompiler(getConfig().markup.language)?.compiledCodeLanguage || 'html',
- style: getLanguageCompiler(getConfig().style.language)?.compiledCodeLanguage || 'css',
- script: getLanguageCompiler(getConfig().script.language)?.compiledCodeLanguage || 'javascript',
+ markup: getCompiledCodeLanguage(config.markup.language, config) || 'html',
+ style: getCompiledCodeLanguage(config.style.language, config) || 'css',
+ script: getCompiledCodeLanguage(config.script.language, config) || 'javascript',
};
const loadingComments: Partial> = {
diff --git a/src/livecodes/export/utils.ts b/src/livecodes/export/utils.ts
index b08352c5f6..9ac7980387 100644
--- a/src/livecodes/export/utils.ts
+++ b/src/livecodes/export/utils.ts
@@ -152,7 +152,10 @@ export const getContent = ({
),
};
- const scriptType = getLanguageCompiler?.(config.script.language)?.scriptType;
+ let scriptType = getLanguageCompiler?.(config.script.language)?.scriptType;
+ if (typeof scriptType === 'function') {
+ scriptType = scriptType({ config });
+ }
if (!isScriptSupported && scriptType && scriptType !== 'module') {
if (editorId === 'markup') {
return (
diff --git a/src/livecodes/languages/nim/__tests__/lang-nim.spec.ts b/src/livecodes/languages/nim/__tests__/lang-nim.spec.ts
new file mode 100644
index 0000000000..285b89d0da
--- /dev/null
+++ b/src/livecodes/languages/nim/__tests__/lang-nim.spec.ts
@@ -0,0 +1,104 @@
+import type { Compiler, CompileResult, Config, Language } from '../../../models';
+import { nim } from '../lang-nim';
+
+const compiler = nim.compiler as Compiler;
+
+const configWith = (customSettings: Config['customSettings']) =>
+ ({ customSettings }) as unknown as Config;
+
+type Configured = T | ((options: { config: Config }) => T | undefined);
+
+const resolveConfig = (value: Configured | undefined, config: Config): T | undefined =>
+ typeof value === 'function'
+ ? (value as (options: { config: Config }) => T | undefined)({ config })
+ : value;
+
+const scriptsFor = (config: Config) =>
+ (
+ compiler.scripts as (options: { compiled: string; baseUrl: string; config: Config }) => string[]
+ )({ compiled: '', baseUrl: 'https://example.test/livecodes/', config });
+
+describe('nim custom settings', () => {
+ test('defaults to the JavaScript backend', () => {
+ const config = configWith({});
+ expect(resolveConfig(compiler.compiledCodeLanguage, config)).toBe('javascript');
+ expect(resolveConfig(compiler.scriptType, config)).toBeUndefined();
+ expect(scriptsFor(config)).toEqual([]);
+ });
+
+ test('selects C (Wasm) with target "c"', () => {
+ const config = configWith({ nim: { target: 'c' } });
+ expect(resolveConfig(compiler.compiledCodeLanguage, config)).toBe('c-wasm');
+ expect(resolveConfig(compiler.scriptType, config)).toBe('text/c-wasm');
+ expect(scriptsFor(config)).toEqual([
+ 'https://example.test/livecodes/{{hash:lang-clang-wasm-script.js}}',
+ ]);
+ });
+
+ test('selects C++ (Wasm) with target "cpp"', () => {
+ const config = configWith({ nim: { target: 'cpp' } });
+ expect(resolveConfig(compiler.compiledCodeLanguage, config)).toBe('cpp-wasm');
+ expect(resolveConfig(compiler.scriptType, config)).toBe('text/cpp-wasm');
+ expect(scriptsFor(config)[0]).toContain('lang-clang-wasm-script.js');
+ });
+});
+
+type Compile = (code: string, options: { config: Config }) => Promise;
+
+const createCompilerMock = () => {
+ const calls: Array> = [];
+ const globalObject = globalThis as unknown as Record;
+ if (globalObject.self == null) globalObject.self = globalObject;
+ (globalObject.self as Record).nimWasm = {
+ createCompiler: (options: Record) => {
+ calls.push(options);
+ return Promise.resolve({
+ run: async () => ({ compiledCode: `compiled-${options.target}` }),
+ });
+ },
+ };
+ return calls;
+};
+
+const compilerFactory = async (): Promise => {
+ const factory = compiler.factory as unknown as (
+ config: Config,
+ baseUrl: string,
+ ) => Promise;
+ return factory(configWith({}), '');
+};
+
+describe('nim compiler factory', () => {
+ test('compiles with the configured target and requests one amalgamated file', async () => {
+ const calls = createCompilerMock();
+ const compile = await compilerFactory();
+ const result = await compile('echo "hi"', {
+ config: configWith({ nim: { target: 'c' } }),
+ });
+
+ expect(calls).toHaveLength(1);
+ expect(calls[0]).toMatchObject({ target: 'c', emit: 'single' });
+ expect(result).toEqual({ code: 'compiled-c', info: {} });
+ });
+
+ test('wraps the JavaScript program in its own scope', async () => {
+ createCompilerMock();
+ const compile = await compilerFactory();
+ const result = await compile('echo "hi"', { config: configWith({}) });
+
+ expect(result).toEqual({ code: '(() => {\ncompiled-js\n})();', info: {} });
+ });
+
+ test('keeps one compiler per target', async () => {
+ const calls = createCompilerMock();
+ const compile = await compilerFactory();
+ const cpp = { config: configWith({ nim: { target: 'cpp' } }) };
+
+ await compile('a', cpp);
+ await compile('b', cpp);
+ expect(calls).toHaveLength(1);
+
+ await compile('c', { config: configWith({ nim: { target: 'c' } }) });
+ expect(calls.map((call) => call.target)).toEqual(['cpp', 'c']);
+ });
+});
diff --git a/src/livecodes/languages/nim/lang-nim.ts b/src/livecodes/languages/nim/lang-nim.ts
index 6e57e5a86a..1b99cdc803 100644
--- a/src/livecodes/languages/nim/lang-nim.ts
+++ b/src/livecodes/languages/nim/lang-nim.ts
@@ -1,5 +1,6 @@
-import type { CompileResult, LanguageSpecs } from '../../models';
+import type { Language, LanguageSpecs, ScriptType } from '../../models';
import { codeMirrorBaseUrl, monacoLanguagesBaseUrl, nimWasmBaseUrl } from '../../vendors';
+import { getLanguageCustomSettings } from '../utils';
// 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, the same
@@ -10,31 +11,50 @@ export const nim: LanguageSpecs = {
compiler: {
url: nimWasmBaseUrl + 'dist/nim-wasm.global.js',
factory: () => {
- let compiler: Promise | null = null;
- const getCompiler = () => {
- if (!compiler) {
+ const compilers: Record<'js' | 'c' | 'cpp', Promise | null> = {
+ js: null,
+ c: null,
+ cpp: null,
+ };
+ const getCompiler = (target: 'js' | 'c' | 'cpp' = 'js') => {
+ if (!compilers[target]) {
const created: Promise = (self as any).nimWasm.createCompiler({
- target: 'js',
+ target,
+ emit: 'single',
baseUrl: nimWasmBaseUrl + 'assets/nim/',
});
// do not keep a failed boot (e.g. a transient network error)
created.catch(() => {
- compiler = null;
+ compilers[target] = null;
});
- compiler = created;
+ compilers[target] = created;
}
- return compiler;
+ return compilers[target];
};
- return async (code: string): Promise => {
- const result = await (await getCompiler()).run(code, '', { execute: false });
+ return async (code: string, { config }) => {
+ const target = getLanguageCustomSettings('nim', config)?.target || 'js';
+ const result = await (await getCompiler(target)).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: {} };
+ if (target === 'js') {
+ return { code: '(() => {\n' + result.compiledCode + '\n})();', info: {} };
+ }
+ return { code: result.compiledCode, info: {} };
};
},
+ compiledCodeLanguage: ({ config }) => {
+ const target = getLanguageCustomSettings('nim', config)?.target || 'js';
+ return target === 'js' ? 'javascript' : ((target + '-wasm') as Language);
+ },
+ scripts: ({ baseUrl, config }) => {
+ const target = getLanguageCustomSettings('nim', config)?.target || 'js';
+ return target === 'js' ? [] : [baseUrl + '{{hash:lang-clang-wasm-script.js}}'];
+ },
+ scriptType: ({ config }) => {
+ const target = getLanguageCustomSettings('nim', config)?.target || 'js';
+ return target === 'js' ? undefined : (`text/${target}-wasm` as ScriptType);
+ },
},
extensions: ['nim', 'nims', 'nimble', 'nim-js', 'js.nim'],
editor: 'script',
diff --git a/src/livecodes/models.ts b/src/livecodes/models.ts
index c34664f852..0eb72ed690 100644
--- a/src/livecodes/models.ts
+++ b/src/livecodes/models.ts
@@ -188,55 +188,10 @@ export interface Compiler {
| ((options: { baseUrl: string; config: Config }) => string | Promise);
inlineModule?: string | ((options: { baseUrl: string }) => Promise);
loadAsExternalModule?: boolean;
- scriptType?:
- | 'module'
- | 'text/liquid'
- | 'text/python'
- | 'text/r'
- | 'text/ruby-wasm'
- | 'text/x-uniter-php'
- | 'text/php-wasm'
- | 'text/c-wasm'
- | 'text/cpp'
- | 'text/cpp-wasm'
- | 'text/objc-wasm'
- | 'text/objcpp-wasm'
- | 'text/nim-wasm'
- | 'text/janet-wasm'
- | 'text/fortran-wasm'
- | 'text/d-wasm'
- | 'text/cobol-wasm'
- | 'text/rust-wasm'
- | 'text/crystal-wasm'
- | 'text/odin-wasm'
- | 'text/zig-wasm'
- | 'text/java'
- | 'text/csharp-wasm'
- | 'text/razor'
- | 'text/fanak'
- | 'text/vb-wasm'
- | 'text/fsharp-wasm'
- | 'text/perl'
- | 'text/julia'
- | 'text/biwascheme'
- | 'text/commonlisp'
- | 'text/tcl'
- | 'text/prolog'
- | 'text/haskell'
- | 'text/haskell-wasm'
- | 'text/minizinc'
- | 'text/go-wasm'
- | 'text/swift-wasm'
- | 'text/ada-wasm'
- | 'text/v-wasm'
- | 'text/assembly'
- | 'application/json'
- | 'application/lua'
- | 'text/fennel'
- | 'application/wasm-uint8';
+ scriptType?: ScriptType | ((options: { config: Config }) => ScriptType | undefined);
liveReload?: boolean;
aliasTo?: Language;
- compiledCodeLanguage?: Language;
+ compiledCodeLanguage?: Language | ((options: { config: Config }) => Language | undefined);
imports?: { [key: string]: string };
types?: Types;
}
@@ -245,6 +200,53 @@ export interface Compilers {
[language: string]: Compiler;
}
+export type ScriptType =
+ | 'module'
+ | 'text/liquid'
+ | 'text/python'
+ | 'text/r'
+ | 'text/ruby-wasm'
+ | 'text/x-uniter-php'
+ | 'text/php-wasm'
+ | 'text/c-wasm'
+ | 'text/cpp'
+ | 'text/cpp-wasm'
+ | 'text/objc-wasm'
+ | 'text/objcpp-wasm'
+ | 'text/nim-wasm'
+ | 'text/janet-wasm'
+ | 'text/fortran-wasm'
+ | 'text/d-wasm'
+ | 'text/cobol-wasm'
+ | 'text/rust-wasm'
+ | 'text/crystal-wasm'
+ | 'text/odin-wasm'
+ | 'text/zig-wasm'
+ | 'text/java'
+ | 'text/csharp-wasm'
+ | 'text/razor'
+ | 'text/fanak'
+ | 'text/vb-wasm'
+ | 'text/fsharp-wasm'
+ | 'text/perl'
+ | 'text/julia'
+ | 'text/biwascheme'
+ | 'text/commonlisp'
+ | 'text/tcl'
+ | 'text/prolog'
+ | 'text/haskell'
+ | 'text/haskell-wasm'
+ | 'text/minizinc'
+ | 'text/go-wasm'
+ | 'text/swift-wasm'
+ | 'text/ada-wasm'
+ | 'text/v-wasm'
+ | 'text/assembly'
+ | 'application/json'
+ | 'application/lua'
+ | 'text/fennel'
+ | 'application/wasm-uint8';
+
export type TemplateAlias =
| 'js'
| 'ts'
diff --git a/src/livecodes/result/result-page.ts b/src/livecodes/result/result-page.ts
index 3317d86440..51e20d2447 100644
--- a/src/livecodes/result/result-page.ts
+++ b/src/livecodes/result/result-page.ts
@@ -442,8 +442,12 @@ export const createResultPage = async ({
scriptElement.dataset.livecodesScript = 'editor';
const languageScriptType = getLanguageCompiler(code.script.language)?.scriptType;
+ const resolvedLanguageScriptType =
+ typeof languageScriptType === 'function'
+ ? languageScriptType({ config })
+ : languageScriptType;
const scriptType =
- languageScriptType ||
+ resolvedLanguageScriptType ||
config.customSettings.scriptType ||
(isModuleScript(script) ? 'module' : undefined);
diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts
index 8fb049652d..2942062e0c 100644
--- a/src/livecodes/vendors.ts
+++ b/src/livecodes/vendors.ts
@@ -385,7 +385,7 @@ 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 nimWasmBaseUrl = /* @__PURE__ */ getUrl('@live-codes/nim-wasm@0.5.0/');
export const ninjaKeysUrl = /* @__PURE__ */ getUrl('@hatemhosny/ninja-keys@1.14.0/bundle/index.js');