Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 43 additions & 6 deletions docs/docs/languages/nim.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -27,9 +30,25 @@ export const nimConfig = {

<LiveCodes config={nimConfig}></LiveCodes>

### 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
Expand Down Expand Up @@ -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

Expand All @@ -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
5 changes: 4 additions & 1 deletion src/livecodes/UI/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';

Expand Down
25 changes: 19 additions & 6 deletions src/livecodes/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -897,17 +897,26 @@ 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 } = {
markup: 'html',
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 } } = {
Expand Down Expand Up @@ -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) =>
Expand Down Expand Up @@ -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<Record<Language, string>> = {
Expand Down
5 changes: 4 additions & 1 deletion src/livecodes/export/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
104 changes: 104 additions & 0 deletions src/livecodes/languages/nim/__tests__/lang-nim.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import type { Compiler, CompileResult, Config, Language } from '../../../models';
Comment thread
coderabbitai[bot] marked this conversation as resolved.
import { nim } from '../lang-nim';

const compiler = nim.compiler as Compiler;

const configWith = (customSettings: Config['customSettings']) =>
({ customSettings }) as unknown as Config;

type Configured<T> = T | ((options: { config: Config }) => T | undefined);

const resolveConfig = <T>(value: Configured<T> | 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<Language>(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<Language>(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<Language>(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<string | CompileResult>;

const createCompilerMock = () => {
const calls: Array<Record<string, unknown>> = [];
const globalObject = globalThis as unknown as Record<string, unknown>;
if (globalObject.self == null) globalObject.self = globalObject;
(globalObject.self as Record<string, unknown>).nimWasm = {
createCompiler: (options: Record<string, unknown>) => {
calls.push(options);
return Promise.resolve({
run: async () => ({ compiledCode: `compiled-${options.target}` }),
});
},
};
return calls;
};

const compilerFactory = async (): Promise<Compile> => {
const factory = compiler.factory as unknown as (
config: Config,
baseUrl: string,
) => Promise<Compile>;
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']);
});
});
46 changes: 33 additions & 13 deletions src/livecodes/languages/nim/lang-nim.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -10,31 +11,50 @@ export const nim: LanguageSpecs = {
compiler: {
url: nimWasmBaseUrl + 'dist/nim-wasm.global.js',
factory: () => {
let compiler: Promise<any> | null = null;
const getCompiler = () => {
if (!compiler) {
const compilers: Record<'js' | 'c' | 'cpp', Promise<any> | null> = {
js: null,
c: null,
cpp: null,
};
const getCompiler = (target: 'js' | 'c' | 'cpp' = 'js') => {
if (!compilers[target]) {
const created: Promise<any> = (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<CompileResult> => {
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',
Expand Down
Loading
Loading