const loadMonacoLanguage = async (lang: Language) => {
const langSupport = getLanguageSpecs(lang)?.editorSupport?.monaco?.languageSupport;
- if (langSupport && !loadedLanguages.has(lang)) {
- loadedLanguages.add(lang);
+ const mappedLanguage = mapLanguage(lang, 'monaco');
+ if (langSupport && !loadedLanguages.has(mappedLanguage)) {
+ loadedLanguages.add(mappedLanguage);
const loadLanguage =
typeof langSupport === 'string'
? (await import(langSupport)).default
diff --git a/src/livecodes/html/language-info.html b/src/livecodes/html/language-info.html
index 33fc4aae7..da69a65b2 100644
--- a/src/livecodes/html/language-info.html
+++ b/src/livecodes/html/language-info.html
@@ -341,6 +341,39 @@ C# (Wasm)
+
+
+ Zig (Wasm)
+
+ Zig compiler running on WebAssembly.
+
+
+
+
Diagrams
(Experimental)
diff --git a/src/livecodes/i18n/locales/en/language-info.lokalise.json b/src/livecodes/i18n/locales/en/language-info.lokalise.json
index ecb4d794b..7fe8c02dc 100644
--- a/src/livecodes/i18n/locales/en/language-info.lokalise.json
+++ b/src/livecodes/i18n/locales/en/language-info.lokalise.json
@@ -1071,5 +1071,17 @@
"wat.name": {
"notes": "",
"translation": "WebAssembly Text Format"
+ },
+ "zigWasm.desc": {
+ "notes": "",
+ "translation": "Zig compiler running on WebAssembly."
+ },
+ "zigWasm.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": " Zig website Zig language documentation Learn X in Y minutes, where X=Zig LiveCodes Documentation Load starter template "
+ },
+ "zigWasm.name": {
+ "notes": "",
+ "translation": "Zig (Wasm)"
}
}
diff --git a/src/livecodes/i18n/locales/en/language-info.ts b/src/livecodes/i18n/locales/en/language-info.ts
index e4abb00e1..a17ee49be 100644
--- a/src/livecodes/i18n/locales/en/language-info.ts
+++ b/src/livecodes/i18n/locales/en/language-info.ts
@@ -459,6 +459,11 @@ const languageInfo = {
link: '<1><2>WebAssembly.org2>1> <3> <4>WebAssembly Text Specs4> 3> <5> <6>WebAssembly on MDN6> 5> <7> <8>Understanding WebAssembly text format8> 7> <9> <10>wabt.js documentation10> 9> <11> <12>Learn X in Y minutes, where X=WebAssembly12> 11> <13> <14>Load starter template14> 13>',
name: 'WebAssembly Text Format',
},
+ zigWasm: {
+ desc: 'Zig compiler running on WebAssembly.',
+ link: '<1> <2>Zig website2> 1> <3> <4>Zig language documentation4> 3> <5> <6>Learn X in Y minutes, where X=Zig6> 5> <7> <8>LiveCodes Documentation8> 7> <9> <10>Load starter template10> 9>',
+ name: 'Zig (Wasm)',
+ },
} as const satisfies I18nTranslationTemplate;
export default languageInfo;
diff --git a/src/livecodes/i18n/locales/en/translation.lokalise.json b/src/livecodes/i18n/locales/en/translation.lokalise.json
index bbe04bf57..a9bc5f2ba 100644
--- a/src/livecodes/i18n/locales/en/translation.lokalise.json
+++ b/src/livecodes/i18n/locales/en/translation.lokalise.json
@@ -2836,6 +2836,10 @@
"notes": "",
"translation": "WebAssembly Text Starter"
},
+ "templates.starter.zig-wasm": {
+ "notes": "",
+ "translation": "Zig (Wasm) Starter"
+ },
"templates.user.heading": {
"notes": "",
"translation": "My Templates"
diff --git a/src/livecodes/i18n/locales/en/translation.ts b/src/livecodes/i18n/locales/en/translation.ts
index 84129fe45..085d53a53 100644
--- a/src/livecodes/i18n/locales/en/translation.ts
+++ b/src/livecodes/i18n/locales/en/translation.ts
@@ -1055,6 +1055,7 @@ const translation = {
vue: 'Vue SFC Starter',
vue2: 'Vue 2 Starter',
wat: 'WebAssembly Text Starter',
+ 'zig-wasm': 'Zig (Wasm) Starter',
},
user: {
heading: 'My Templates',
diff --git a/src/livecodes/languages/languages.ts b/src/livecodes/languages/languages.ts
index 9921f547a..2cdfec143 100644
--- a/src/livecodes/languages/languages.ts
+++ b/src/livecodes/languages/languages.ts
@@ -82,6 +82,8 @@ import { vento } from './vento';
import { vue, vueApp } from './vue';
import { vue2 } from './vue2';
import { wat } from './wat';
+import { zigWasm } from './zig-wasm';
+
export const languages: LanguageSpecs[] = [
html,
markdown,
@@ -151,6 +153,7 @@ export const languages: LanguageSpecs[] = [
phpWasm,
cpp,
cppWasm,
+ zigWasm,
java,
csharpWasm,
fsharp,
diff --git a/src/livecodes/languages/zig-wasm/index.ts b/src/livecodes/languages/zig-wasm/index.ts
new file mode 100644
index 000000000..5de310f3b
--- /dev/null
+++ b/src/livecodes/languages/zig-wasm/index.ts
@@ -0,0 +1 @@
+export * from './lang-zig-wasm';
diff --git a/src/livecodes/languages/zig-wasm/lang-zig-wasm-script.ts b/src/livecodes/languages/zig-wasm/lang-zig-wasm-script.ts
new file mode 100644
index 000000000..3a7ee2b5d
--- /dev/null
+++ b/src/livecodes/languages/zig-wasm/lang-zig-wasm-script.ts
@@ -0,0 +1,586 @@
+import { getErrorMessage, loadScript } from '../../utils/utils';
+import { jsUntarUrl, wasiShimUrl, zigWasmBaseUrl } from '../../vendors';
+// @ts-ignore
+// eslint-disable-next-line import/no-unresolved
+import workerContent from './lang-zig-wasm-worker.raw.js?raw';
+
+interface ZigResult {
+ output: string | null;
+ error: string | null;
+}
+
+interface ZigRunResult extends ZigResult {
+ exitCode: number;
+}
+
+interface ZigRunner {
+ readonly ready: boolean;
+ init(): Promise;
+ run(source: string, input?: string): Promise;
+}
+
+declare const window: Window & {
+ livecodes: {
+ zig: {
+ ready?: boolean;
+ failed?: boolean;
+ error?: string | null;
+ init?: Promise | null;
+ runner?: ZigRunner;
+ loaded?: Promise;
+ input?: string;
+ output?: string | null;
+ exitCode?: number | null;
+ run?: (input?: string) => Promise;
+ };
+ };
+};
+
+// Worker boot includes downloading and extracting the Zig toolchain
+// (tens of MB), so allow generous time before falling back to the main thread.
+const BOOT_TIMEOUT_MS = 180000;
+// A single compile + run is normally quick; a long timeout guards against hangs
+// (e.g. an infinite loop in the user program) by killing and respawning the worker.
+const RUN_TIMEOUT_MS = 120000;
+
+// ---- main-thread runner (fallback) -------------------------------------
+// Used when Web Workers are not available in the result page.
+function createMainThreadRunner(): ZigRunner {
+ let ready = false;
+ let initPromise: Promise | null = null;
+ let untar: any;
+ let wasi: any;
+ let zigStdLib: any;
+ const compiledModules: Map = new Map();
+ // compiled user modules are small; bound the map to avoid unbounded growth
+ // over a long-lived page (live reload reuses the same runner)
+ const MAX_CACHED_MODULES = 16;
+ const isSharedArrayBufferSupported = typeof SharedArrayBuffer !== 'undefined';
+
+ // hashing function for cache validation
+ const createHash = (str: string): number => {
+ let hash = 0;
+ for (let i = 0; i < str.length; i++) {
+ const char = str.charCodeAt(i);
+ hash = (hash * 31 + char) % 0x7fffffff;
+ }
+ return hash;
+ };
+
+ const ensureDependencies = async (): Promise => {
+ if (!untar) {
+ untar = await loadScript(jsUntarUrl, 'untar');
+ if (!untar) throw new Error('js-untar failed to load');
+ }
+
+ if (!wasi) {
+ wasi = await import(wasiShimUrl);
+ }
+ };
+
+ const createFile = (data: ArrayBuffer | Uint8Array): any => {
+ const dataArray = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
+
+ if (isSharedArrayBufferSupported) {
+ const sharedBuffer = new SharedArrayBuffer(dataArray.length);
+ const view = new Uint8Array(sharedBuffer);
+ view.set(dataArray);
+
+ return {
+ get data() {
+ return view;
+ },
+ get buffer() {
+ return sharedBuffer;
+ },
+ };
+ }
+ const { File } = wasi;
+ return new File(dataArray);
+ };
+
+ const processZigArchive = async (response: Response): Promise => {
+ const { Directory } = wasi;
+
+ const arrayBuffer = await response.arrayBuffer();
+ const entries = await untar(arrayBuffer);
+ const fsMap = new Map();
+
+ for (const entry of entries) {
+ if (!entry.name.startsWith('lib/') || entry.type !== '0' || !entry.buffer) continue;
+
+ const path = entry.name.substring(4).split('/');
+ let current = fsMap;
+
+ for (let i = 0; i < path.length - 1; i++) {
+ if (!current.has(path[i])) current.set(path[i], new Map());
+ current = current.get(path[i]);
+ }
+
+ const fileData = createFile(entry.buffer);
+
+ current.set(path[path.length - 1], fileData);
+ }
+
+ const buildDirectory = (map: Map): any =>
+ new Directory(
+ Array.from(map.entries()).map(([key, value]) => [
+ key,
+ value instanceof Map ? buildDirectory(value) : value,
+ ]),
+ );
+
+ return buildDirectory(fsMap);
+ };
+
+ const doInit = async (): Promise => {
+ await ensureDependencies();
+
+ const response = await fetch(`${zigWasmBaseUrl}zig.tar.gz`);
+ if (!response.ok) throw new Error(`Failed to fetch zig.tar.gz: ${response.statusText}`);
+
+ const arrayBuffer = await response.arrayBuffer();
+
+ const decompressed = new Response(
+ new ReadableStream({
+ start(controller) {
+ controller.enqueue(new Uint8Array(arrayBuffer));
+ controller.close();
+ },
+ }).pipeThrough(new DecompressionStream('gzip')),
+ );
+
+ zigStdLib = await processZigArchive(decompressed);
+ ready = true;
+ };
+
+ const init = async (): Promise => {
+ if (ready) return;
+ if (!initPromise) {
+ initPromise = doInit().catch((err) => {
+ initPromise = null;
+ throw err;
+ });
+ }
+ return initPromise;
+ };
+
+ // compile the user code to a WebAssembly module (cached for identical source,
+ // e.g. interactive re-runs that only provide new stdin)
+ const compileZig = async (code: string): Promise => {
+ const hash = createHash(code);
+ const cached = compiledModules.get(hash);
+ if (cached && cached.source === code) return cached.module;
+
+ const { WASI, OpenFile, PreopenDirectory, ConsoleStdout } = wasi;
+ const errors: string[] = [];
+ const stderr = ConsoleStdout.lineBuffered((line: string) => errors.push(line));
+
+ const codeFile = createFile(new TextEncoder().encode(code));
+
+ const compileWasi = new WASI(
+ [
+ 'zig.wasm',
+ 'build-exe',
+ 'main.zig',
+ '-target',
+ 'wasm32-wasi',
+ '-fno-entry',
+ '-O',
+ 'ReleaseSmall',
+ ],
+ [],
+ [
+ new OpenFile(createFile(new TextEncoder().encode(''))),
+ createFile(new TextEncoder().encode('')),
+ stderr,
+ new PreopenDirectory('.', new Map([['main.zig', codeFile]])),
+ new PreopenDirectory('/lib', zigStdLib.contents),
+ new PreopenDirectory('/cache', new Map()),
+ ],
+ { debug: false },
+ );
+
+ const wasmResponse = await fetch(`${zigWasmBaseUrl}zig.wasm`);
+ const wasmArrayBuffer = await wasmResponse.arrayBuffer();
+
+ let compilerModule: WebAssembly.Module;
+ if (isSharedArrayBufferSupported) {
+ const sharedWasmBuffer = new SharedArrayBuffer(wasmArrayBuffer.byteLength);
+ const sharedWasmView = new Uint8Array(sharedWasmBuffer);
+ sharedWasmView.set(new Uint8Array(wasmArrayBuffer));
+ compilerModule = await WebAssembly.compile(sharedWasmView as any);
+ } else {
+ compilerModule = await WebAssembly.compile(wasmArrayBuffer);
+ }
+
+ const instance = await WebAssembly.instantiate(compilerModule, {
+ wasi_snapshot_preview1: compileWasi.wasiImport,
+ });
+
+ const exitCode = compileWasi.start(instance);
+ if (exitCode !== 0) {
+ throw new Error(`Compilation failed: ${errors.join('\n')}`);
+ }
+
+ const mainWasm = (compileWasi.fds[3] as any).dir.contents.get('main.wasm');
+ if (!mainWasm?.data) throw new Error('Compilation output not found');
+
+ const wasmData =
+ mainWasm.data instanceof Uint8Array ? mainWasm.data : new Uint8Array(mainWasm.data);
+
+ const module = await WebAssembly.compile(wasmData);
+ if (compiledModules.size >= MAX_CACHED_MODULES) {
+ compiledModules.clear();
+ }
+ compiledModules.set(hash, { source: code, module });
+ return module;
+ };
+
+ const runZig = async (code: string, input = ''): Promise => {
+ const output: string[] = [];
+ const errors: string[] = [];
+
+ try {
+ await init();
+ if (!zigStdLib) {
+ return { output: null, error: 'Zig environment not ready' };
+ }
+
+ const { WASI, OpenFile, PreopenDirectory, ConsoleStdout } = wasi;
+
+ const stdout = ConsoleStdout.lineBuffered((line: string) => output.push(line));
+ const stderr = ConsoleStdout.lineBuffered((line: string) => errors.push(line));
+
+ const module = await compileZig(code);
+
+ const inputFile = createFile(new TextEncoder().encode(input));
+
+ const executeWasi = new WASI(
+ ['main.wasm'],
+ [],
+ [new OpenFile(inputFile), stdout, stderr, new PreopenDirectory('.', new Map())],
+ { debug: false },
+ );
+
+ const instance = await WebAssembly.instantiate(module, {
+ wasi_snapshot_preview1: executeWasi.wasiImport,
+ });
+
+ executeWasi.start(instance);
+ const outputStr = output.join('\n') || null;
+ const errorStr = errors.join('\n') || null;
+
+ return { output: outputStr, error: errorStr };
+ } catch (err) {
+ compiledModules.clear();
+ const error = 'Error: ' + getErrorMessage(err);
+ return { output: null, error };
+ }
+ };
+
+ const run = (source: string, input?: string): Promise => runZig(source, input ?? '');
+
+ return {
+ init,
+ run,
+ get ready() {
+ return ready;
+ },
+ };
+}
+
+// ---- worker runner -------------------------------------------------------
+function createWorkerRunner(): ZigRunner {
+ let worker: Worker | null = null;
+ let readyPromise: Promise | null = null;
+ let resolveReady: (() => void) | null = null;
+ let rejectReady: ((error?: Error) => void) | null = null;
+ let pending: {
+ [id: number]: {
+ resolve: (result: ZigResult) => void;
+ reject: (error: Error) => void;
+ timer: ReturnType;
+ };
+ } = {};
+ let nextId = 1;
+
+ function onMessage(e: MessageEvent) {
+ const msg = e.data || {};
+ if (msg.type === 'ready') {
+ if (resolveReady) {
+ resolveReady();
+ resolveReady = null;
+ rejectReady = null;
+ }
+ } else if (msg.type === 'fatal') {
+ // Boot failed — tear down and reject init so the caller falls back.
+ if (worker) {
+ worker.terminate();
+ worker = null;
+ }
+ if (rejectReady) {
+ rejectReady(new Error('Zig worker failed to start: ' + msg.message));
+ resolveReady = null;
+ rejectReady = null;
+ }
+ } else if (msg.type === 'result') {
+ const p = pending[msg.id];
+ if (p) {
+ delete pending[msg.id];
+ clearTimeout(p.timer);
+ p.resolve(JSON.parse(msg.json));
+ }
+ } else if (msg.type === 'error') {
+ const pe = pending[msg.id];
+ if (pe) {
+ delete pending[msg.id];
+ clearTimeout(pe.timer);
+ pe.reject(new Error(msg.message));
+ }
+ }
+ }
+
+ function onError(err: Error) {
+ rejectAll(new Error('Zig worker crashed: ' + (err && err.message ? err.message : 'unknown')));
+ if (worker) {
+ worker.terminate();
+ worker = null;
+ }
+ }
+
+ function rejectAll(err: Error) {
+ for (const id in pending) {
+ if (Object.prototype.hasOwnProperty.call(pending, id)) {
+ const p = pending[id];
+ clearTimeout(p.timer);
+ p.reject(err);
+ }
+ }
+ pending = {};
+ }
+
+ function spawn() {
+ readyPromise = new Promise(function (resolve, reject) {
+ resolveReady = resolve;
+ rejectReady = reject;
+ });
+ try {
+ const toDataUrl = (content: string, type = 'text/javascript') =>
+ `data:${type};charset=UTF-8;base64,` + btoa(content);
+
+ const workerUrl = toDataUrl(
+ `self.zigWasmBaseUrl = "${zigWasmBaseUrl}"; self.wasiShimUrl = "${wasiShimUrl}";\n\n${workerContent}`,
+ );
+ worker = new Worker(workerUrl);
+ worker.onmessage = onMessage;
+ worker.onerror = onError as any;
+ } catch (err) {
+ // Workers unavailable in this context (e.g. sandbox restrictions).
+ worker = null;
+ readyPromise = null;
+ resolveReady = null;
+ rejectReady = null;
+ throw err;
+ }
+ }
+
+ function ensureWorker() {
+ if (!worker) spawn();
+ }
+
+ function init(): Promise {
+ try {
+ ensureWorker();
+ } catch (err) {
+ return Promise.reject(err);
+ }
+ return new Promise(function (resolve, reject) {
+ const timer = setTimeout(function () {
+ if (worker) {
+ worker.terminate();
+ worker = null;
+ }
+ reject(new Error('Zig worker failed to start (timeout)'));
+ }, BOOT_TIMEOUT_MS);
+ readyPromise?.then(
+ function () {
+ clearTimeout(timer);
+ resolve();
+ },
+ function (err) {
+ clearTimeout(timer);
+ reject(err);
+ },
+ );
+ });
+ }
+
+ function run(source: string, input?: string): Promise {
+ try {
+ ensureWorker();
+ } catch (err) {
+ return Promise.reject(err);
+ }
+ if (!readyPromise) return Promise.reject(new Error('Zig worker is not ready'));
+ return readyPromise.then(function () {
+ if (!worker) {
+ // the worker died while we were waiting (boot failure); respawn it
+ spawn();
+ return readyPromise!.then(() => runNow(source, input));
+ }
+ return runNow(source, input);
+ });
+ }
+
+ function runNow(source: string, input?: string): Promise {
+ const id = nextId++;
+ return new Promise(function (resolve, reject) {
+ const timer = setTimeout(function () {
+ // the worker hung — kill it and let the next run respawn
+ delete pending[id];
+ if (worker) {
+ worker.terminate();
+ worker = null;
+ }
+ reject(new Error('Zig compile timed out; restarted the compiler.'));
+ }, RUN_TIMEOUT_MS);
+ pending[id] = { resolve, reject, timer };
+ worker?.postMessage({ type: 'compile', source, stdin: input ?? '', id });
+ });
+ }
+
+ return {
+ init,
+ run,
+ get ready() {
+ return !!worker && resolveReady === null;
+ },
+ };
+}
+
+// ---- choose runner --------------------------------------------------------
+function isNode() {
+ return (
+ typeof process !== 'undefined' && process.versions != null && process.versions.node != null
+ );
+}
+
+function createRunner(): ZigRunner {
+ const useWorker = !isNode() && typeof Worker === 'function';
+ let runner: ZigRunner | null = useWorker ? createWorkerRunner() : null;
+ const mainRunner = createMainThreadRunner();
+ let fellBack = false;
+
+ function init(): Promise {
+ if (runner && !fellBack) {
+ return runner.init().catch(function (err) {
+ // eslint-disable-next-line no-console
+ console.error('Zig worker unavailable, falling back to main thread:', getErrorMessage(err));
+ fellBack = true;
+ runner = null;
+ return mainRunner.init();
+ });
+ }
+ return mainRunner.init();
+ }
+
+ function run(source: string, input?: string): Promise {
+ return init().then(function () {
+ return (runner && !fellBack ? runner : mainRunner).run(source, input);
+ });
+ }
+
+ return {
+ init,
+ run,
+ get ready() {
+ return runner && !fellBack ? runner.ready : mainRunner.ready;
+ },
+ };
+}
+
+// ---- LiveCodes integration ----------------------------------------------
+window.livecodes.zig ??= {};
+
+const livecodesApi = window.livecodes.zig;
+livecodesApi.ready = false;
+livecodesApi.runner ??= createRunner();
+
+livecodesApi.init ??= (function () {
+ if (livecodesApi.ready) return;
+ parent.postMessage({ type: 'loading', payload: true }, '*');
+ return livecodesApi.runner
+ ?.init()
+ .then(function () {
+ parent.postMessage({ type: 'loading', payload: false }, '*');
+ })
+ .catch(function (err) {
+ parent.postMessage({ type: 'loading', payload: false }, '*');
+ livecodesApi.ready = false;
+ livecodesApi.init = null;
+ livecodesApi.failed = true;
+ livecodesApi.error = getErrorMessage(err);
+ // eslint-disable-next-line no-console
+ console.error('Failed to initialize Zig environment:', err);
+ throw err;
+ });
+})();
+
+livecodesApi.run = async function (input: string | undefined) {
+ let code = '';
+ const scripts = document.querySelectorAll('script[type="text/zig-wasm"]');
+ scripts.forEach(function (script) {
+ code += script.innerHTML + '\n';
+ });
+ if (!code.trim()) return { output: null, error: null, exitCode: 0 };
+
+ livecodesApi.input = input;
+ livecodesApi.output = null;
+ livecodesApi.ready = false;
+ try {
+ await livecodesApi.init;
+ const result = await livecodesApi.runner?.run(code, input);
+ if (!result) throw new Error('Zig runner is not available');
+ const error = result.error;
+ livecodesApi.output = result.output;
+ livecodesApi.error = error;
+ livecodesApi.exitCode = error ? 1 : 0;
+ livecodesApi.ready = true;
+ if (error != null) {
+ // eslint-disable-next-line no-console
+ console.error(error);
+ } else if (result.output != null) {
+ // eslint-disable-next-line no-console
+ console.log(result.output);
+ }
+ return { output: result.output, error, exitCode: error ? 1 : 0 };
+ } catch (err: any) {
+ const msg = 'Error: ' + (err && err.message ? err.message : String(err));
+ livecodesApi.output = null;
+ livecodesApi.error = msg;
+ livecodesApi.exitCode = 1;
+ livecodesApi.ready = true;
+ // eslint-disable-next-line no-console
+ console.error(msg);
+ return { output: null, error: msg, exitCode: 1 };
+ }
+};
+
+livecodesApi.loaded = new Promise((resolve, reject) => {
+ const interval = setInterval(function () {
+ if (livecodesApi.failed) {
+ clearInterval(interval);
+ reject(new Error(livecodesApi.error || 'Failed to initialize the Zig environment'));
+ } else if (livecodesApi.ready) {
+ clearInterval(interval);
+ resolve();
+ }
+ }, 50);
+});
+
+window.addEventListener('load', async function () {
+ parent.postMessage({ type: 'loading', payload: true }, '*');
+ await livecodesApi.run?.(livecodesApi.input);
+ parent.postMessage({ type: 'loading', payload: false }, '*');
+});
diff --git a/src/livecodes/languages/zig-wasm/lang-zig-wasm-worker.raw.js b/src/livecodes/languages/zig-wasm/lang-zig-wasm-worker.raw.js
new file mode 100644
index 000000000..e65199997
--- /dev/null
+++ b/src/livecodes/languages/zig-wasm/lang-zig-wasm-worker.raw.js
@@ -0,0 +1,284 @@
+// @ts-nocheck
+(async function () {
+ let ready = false;
+ let wasi = null;
+ let zigStdLib = null;
+ let zigCompilerModule = null;
+ /** @type {any[]} */
+ const queue = [];
+ /** @type {Map} */
+ const compiledModules = new Map();
+ // compiled user modules are small; bound the map to avoid unbounded growth
+ // when many distinct sources are compiled over a long-lived worker
+ const MAX_CACHED_MODULES = 16;
+
+ // injected by the main thread when the worker is created
+ const { wasiShimUrl, zigWasmBaseUrl } = self;
+ const isSharedArrayBufferSupported = typeof SharedArrayBuffer !== 'undefined';
+
+ // hashing function for cache validation
+ const createHash = (str) => {
+ let hash = 0;
+ for (let i = 0; i < str.length; i++) {
+ const char = str.charCodeAt(i);
+ hash = (hash * 31 + char) % 0x7fffffff;
+ }
+ return hash;
+ };
+
+ const getErrorMessage = (err) => (err && err.message) || String(err);
+
+ const createFile = (data) => {
+ const dataArray = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
+
+ if (isSharedArrayBufferSupported) {
+ const sharedBuffer = new SharedArrayBuffer(dataArray.length);
+ const view = new Uint8Array(sharedBuffer);
+ view.set(dataArray);
+
+ return {
+ get data() {
+ return view;
+ },
+ get buffer() {
+ return sharedBuffer;
+ },
+ };
+ }
+ return new wasi.File(dataArray);
+ };
+
+ const ensureDependencies = async () => {
+ if (!wasi) {
+ wasi = await import(wasiShimUrl);
+ }
+ };
+
+ // Parse the (plain ustar) stdlib archive. js-untar cannot be used inside a
+ // worker (it references `window`, and cross-origin importScripts is blocked
+ // for data-URL workers), so extract the entries we need directly.
+ const parseTar = (buffer) => {
+ const decoder = new TextDecoder();
+ const entries = [];
+ const readString = (offset, length) => {
+ let end = offset + length;
+ for (let i = offset; i < offset + length; i++) {
+ if (buffer[i] === 0) {
+ end = i;
+ break;
+ }
+ }
+ return decoder.decode(buffer.subarray(offset, end));
+ };
+
+ let offset = 0;
+ while (offset + 512 <= buffer.length) {
+ const name = readString(offset, 100);
+ if (!name) break; // end-of-archive marker (zero block)
+
+ const size = parseInt(readString(offset + 124, 12).trim(), 8) || 0;
+ const type = String.fromCharCode(buffer[offset + 156]);
+ const prefix = readString(offset + 345, 155);
+ const dataStart = offset + 512;
+
+ entries.push({
+ name: prefix ? prefix + '/' + name : name,
+ type,
+ data: buffer.subarray(dataStart, dataStart + size),
+ });
+
+ offset = dataStart + Math.ceil(size / 512) * 512;
+ }
+ return entries;
+ };
+
+ const processZigArchive = async (response) => {
+ const arrayBuffer = await response.arrayBuffer();
+ const entries = parseTar(new Uint8Array(arrayBuffer));
+ const fsMap = new Map();
+
+ for (const entry of entries) {
+ if (!entry.name.startsWith('lib/') || entry.type !== '0') continue;
+
+ const path = entry.name.substring(4).split('/');
+ let current = fsMap;
+
+ for (let i = 0; i < path.length - 1; i++) {
+ if (!current.has(path[i])) current.set(path[i], new Map());
+ current = current.get(path[i]);
+ }
+
+ // copy the bytes so each file has its own buffer
+ const fileData = createFile(entry.data.slice());
+
+ current.set(path[path.length - 1], fileData);
+ }
+
+ const buildDirectory = (map) =>
+ new wasi.Directory(
+ Array.from(map.entries()).map(([key, value]) => [
+ key,
+ value instanceof Map ? buildDirectory(value) : value,
+ ]),
+ );
+
+ return buildDirectory(fsMap);
+ };
+
+ const fetchArrayBuffer = async (url) => {
+ const response = await fetch(url);
+ if (!response.ok) throw new Error('Failed to fetch ' + url + ': ' + response.statusText);
+ return response.arrayBuffer();
+ };
+
+ const loadToolchain = async () => {
+ const archiveData = await fetchArrayBuffer(zigWasmBaseUrl + 'zig.tar.gz');
+
+ const decompressed = new Response(
+ new ReadableStream({
+ start(controller) {
+ controller.enqueue(new Uint8Array(archiveData));
+ controller.close();
+ },
+ }).pipeThrough(new DecompressionStream('gzip')),
+ );
+
+ zigStdLib = await processZigArchive(decompressed);
+
+ // pre-compile the compiler once; it is instantiated for every compilation
+ const compilerData = await fetchArrayBuffer(zigWasmBaseUrl + 'zig.wasm');
+ zigCompilerModule = await WebAssembly.compile(compilerData);
+ };
+
+ // compile the user code to a WebAssembly module (cached for identical source,
+ // e.g. interactive re-runs that only provide new stdin)
+ const compileZig = async (code) => {
+ const hash = createHash(code);
+ const cached = compiledModules.get(hash);
+ if (cached && cached.source === code) return cached.module;
+
+ const errors = [];
+ const stderr = wasi.ConsoleStdout.lineBuffered((line) => errors.push(line));
+
+ const codeFile = createFile(new TextEncoder().encode(code));
+
+ const compileWasi = new wasi.WASI(
+ [
+ 'zig.wasm',
+ 'build-exe',
+ 'main.zig',
+ '-target',
+ 'wasm32-wasi',
+ '-fno-entry',
+ '-O',
+ 'ReleaseSmall',
+ ],
+ [],
+ [
+ new wasi.OpenFile(createFile(new TextEncoder().encode(''))),
+ createFile(new TextEncoder().encode('')),
+ stderr,
+ new wasi.PreopenDirectory('.', new Map([['main.zig', codeFile]])),
+ new wasi.PreopenDirectory('/lib', zigStdLib.contents),
+ new wasi.PreopenDirectory('/cache', new Map()),
+ ],
+ { debug: false },
+ );
+
+ const instance = await WebAssembly.instantiate(zigCompilerModule, {
+ wasi_snapshot_preview1: compileWasi.wasiImport,
+ });
+
+ const exitCode = compileWasi.start(instance);
+ if (exitCode !== 0) throw new Error('Compilation failed: ' + errors.join('\n'));
+
+ const mainWasm = compileWasi.fds[3].dir.contents.get('main.wasm');
+ if (!mainWasm || !mainWasm.data) throw new Error('Compilation output not found');
+
+ const wasmData =
+ mainWasm.data instanceof Uint8Array ? mainWasm.data : new Uint8Array(mainWasm.data);
+ const module = await WebAssembly.compile(wasmData);
+
+ if (compiledModules.size >= MAX_CACHED_MODULES) {
+ compiledModules.clear();
+ }
+ compiledModules.set(hash, { source: code, module });
+ return module;
+ };
+
+ // compile and run the user code, returning the same result shape the
+ // main-thread implementation used to produce
+ const runZig = async (code, stdin) => {
+ const output = [];
+ const errors = [];
+
+ try {
+ const module = await compileZig(code);
+
+ const stdout = wasi.ConsoleStdout.lineBuffered((line) => output.push(line));
+ const stderr = wasi.ConsoleStdout.lineBuffered((line) => errors.push(line));
+
+ const inputFile = createFile(new TextEncoder().encode(stdin || ''));
+
+ const executeWasi = new wasi.WASI(
+ ['main.wasm'],
+ [],
+ [new wasi.OpenFile(inputFile), stdout, stderr, new wasi.PreopenDirectory('.', new Map())],
+ { debug: false },
+ );
+
+ const instance = await WebAssembly.instantiate(module, {
+ wasi_snapshot_preview1: executeWasi.wasiImport,
+ });
+
+ executeWasi.start(instance);
+ } catch (err) {
+ compiledModules.clear();
+ return { output: null, error: 'Error: ' + getErrorMessage(err) };
+ }
+
+ return {
+ output: output.join('\n') || null,
+ error: errors.join('\n') || null,
+ };
+ };
+
+ self.onmessage = function (e) {
+ const data = e.data || {};
+ if (data.type !== 'compile') return;
+ if (!ready || !zigStdLib || !zigCompilerModule) queue.push(data);
+ else compile(data);
+ };
+
+ /** @param {{ source: string; stdin?: string; id: number; }} msg */
+ async function compile(msg) {
+ try {
+ const result = await runZig(msg.source, msg.stdin);
+ self.postMessage({ type: 'result', id: msg.id, json: JSON.stringify(result) });
+ } catch (err) {
+ // unexpected failure (runZig handles expected compile/run errors)
+ self.postMessage({
+ type: 'error',
+ id: msg.id,
+ message: String((err && err.stack) || err),
+ });
+ }
+ }
+
+ async function boot() {
+ try {
+ await ensureDependencies();
+ await loadToolchain();
+ ready = true;
+ queue.splice(0).forEach(compile);
+ self.postMessage({ type: 'ready' });
+ } catch (err) {
+ self.postMessage({
+ type: 'fatal',
+ message: String((err && err.stack) || err),
+ });
+ }
+ }
+
+ boot();
+})();
diff --git a/src/livecodes/languages/zig-wasm/lang-zig-wasm.ts b/src/livecodes/languages/zig-wasm/lang-zig-wasm.ts
new file mode 100644
index 000000000..1ce25fdff
--- /dev/null
+++ b/src/livecodes/languages/zig-wasm/lang-zig-wasm.ts
@@ -0,0 +1,26 @@
+import { codemirrorLegacy } from '../../editor/codemirror/utils';
+import type { LanguageSpecs } from '../../models';
+import { codeMirrorBaseUrl, monacoLanguagesBaseUrl } from '../../vendors';
+
+export const zigWasm: LanguageSpecs = {
+ name: 'zig-wasm',
+ title: 'Zig (Wasm)',
+ compiler: {
+ factory: () => async (code) => code,
+ scripts: ({ baseUrl }) => [baseUrl + '{{hash:lang-zig-wasm-script.js}}'],
+ scriptType: 'text/zig-wasm',
+ compiledCodeLanguage: 'zig',
+ liveReload: true,
+ },
+ extensions: ['zig'],
+ editor: 'script',
+ editorSupport: {
+ monaco: { languageSupport: monacoLanguagesBaseUrl + 'zig.js', language: 'zig' },
+ codemirror: {
+ languageSupport: async () =>
+ codemirrorLegacy((await import(codeMirrorBaseUrl + 'codemirror-lang-rust.js')).rust),
+ },
+ codejar: { language: 'zig' },
+ },
+ largeDownload: true,
+};
diff --git a/src/livecodes/models.ts b/src/livecodes/models.ts
index 7fe856507..1910f2808 100644
--- a/src/livecodes/models.ts
+++ b/src/livecodes/models.ts
@@ -204,6 +204,7 @@ export interface Compiler {
| 'text/prolog'
| 'text/minizinc'
| 'text/go-wasm'
+ | 'text/zig-wasm'
| 'application/json'
| 'application/lua'
| 'text/fennel'
diff --git a/src/livecodes/templates/starter/index.ts b/src/livecodes/templates/starter/index.ts
index 9b6813144..7ffaa6b4a 100644
--- a/src/livecodes/templates/starter/index.ts
+++ b/src/livecodes/templates/starter/index.ts
@@ -71,6 +71,7 @@ import { typescriptStarter } from './typescript-starter';
import { vueSfcStarter } from './vue-sfc-starter';
import { vue2Starter } from './vue2-starter';
import { watStarter } from './wat-starter';
+import { zigWasmStarter } from './zig-wasm-starter';
export const starterTemplates = [
blank,
@@ -120,6 +121,7 @@ export const starterTemplates = [
phpWasmStarter,
cppStarter,
cppWasmStarter,
+ zigWasmStarter,
javaStarter,
csharpWasmStarter,
fsharpStarter,
diff --git a/src/livecodes/templates/starter/zig-wasm-starter.ts b/src/livecodes/templates/starter/zig-wasm-starter.ts
new file mode 100644
index 000000000..59b33f79f
--- /dev/null
+++ b/src/livecodes/templates/starter/zig-wasm-starter.ts
@@ -0,0 +1,91 @@
+import type { Template } from '../../models';
+
+export const zigWasmStarter: Template = {
+ name: 'zig-wasm',
+ title: window.deps.translateString('templates.starter.zig-wasm', 'Zig (Wasm) Starter'),
+ thumbnail: 'assets/templates/zig.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: 'zig-wasm',
+ content: `
+const std = @import("std");
+
+pub fn main() !void {
+ const stdout = std.io.getStdOut().writer();
+ const stdin = std.io.getStdIn().reader();
+
+ const title = "Zig";
+ try stdout.print("{s}\\n", .{title});
+
+ var buf: [100]u8 = undefined;
+ const input = try stdin.readUntilDelimiterOrEof(&buf, '\\n');
+ const count = try std.fmt.parseInt(i32, input.?, 10);
+ try stdout.print("{}\\n", .{count + 1});
+}
+`.trimStart(),
+ },
+};
diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts
index e8cfb1903..ccc60da6a 100644
--- a/src/livecodes/vendors.ts
+++ b/src/livecodes/vendors.ts
@@ -279,6 +279,8 @@ export const jsclUrl = /* @__PURE__ */ getUrl(
'gh:jscl-project/jscl-project.github.io@058adc599f0d012718ef3ad28e704a92c4dd741e/jscl.js',
);
+export const jsUntarUrl = /* @__PURE__ */ getUrl('js-untar@2.0.0/build/dist/untar.js');
+
export const jsZipUrl = /* @__PURE__ */ getUrl('jszip@3.10.1/dist/jszip.js');
export const juliaWasmBaseUrl = /* @__PURE__ */ getUrl('@chriskoch/julia-wasm@1.0.4');
@@ -488,8 +490,12 @@ export const vueSfcLoaderCdnBaseUrl = /* @__PURE__ */ getUrl('vue3-sfc-loader@0.
export const wabtjsUrl = /* @__PURE__ */ getUrl('wabt@1.0.35/index.js');
+export const wasiShimUrl = /* @__PURE__ */ getUrl('@bjorn3/browser_wasi_shim@0.3.0/dist/index.js');
+
export const wasmoonUrl = /* @__PURE__ */ getUrl('wasmoon@1.16.0/dist/index.js');
export const waveDromBaseUrl = /* @__PURE__ */ getUrl('wavedrom@3.2.0/');
export const webRBaseUrl = /* @__PURE__ */ getUrl('webr@0.4.0/dist/');
+
+export const zigWasmBaseUrl = /* @__PURE__ */ getUrl('@seth0x41/zig-wasm@1.0.0/');
diff --git a/src/sdk/models.ts b/src/sdk/models.ts
index 720a98fca..069569807 100644
--- a/src/sdk/models.ts
+++ b/src/sdk/models.ts
@@ -213,6 +213,9 @@ export type Language =
| 'edn'
| 'gleam'
| 'swift'
+ | 'rust'
+ | 'zig'
+ | 'zig-wasm'
| 'rescript'
| 'res'
| 'resi'
@@ -403,6 +406,7 @@ export type TemplateName =
| 'csharp-wasm'
| 'fsharp'
| 'fsharp-wasm'
+ | 'zig-wasm'
| 'perl'
| 'lua'
| 'lua-wasm'
@@ -803,6 +807,7 @@ export type CustomSettings = Partial<
| 'text/prolog'
| 'text/minizinc'
| 'text/go-wasm'
+ | 'text/zig-wasm'
| 'application/json'
| 'application/lua'
| 'text/fennel'