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');