diff --git a/src/livecodes/i18n/locales/ar/language-info.lokalise.json b/src/livecodes/i18n/locales/ar/language-info.lokalise.json
index 473773e91..c1db78abf 100644
--- a/src/livecodes/i18n/locales/ar/language-info.lokalise.json
+++ b/src/livecodes/i18n/locales/ar/language-info.lokalise.json
@@ -261,6 +261,15 @@
"ejs.name": {
"translation": "EJS"
},
+ "elixirWasm.desc": {
+ "translation": "مترجم Elixir يعمل على WebAssembly، باستخدام
AtomVM"
+ },
+ "elixirWasm.link": {
+ "translation": "
موقع Elixir وثائق لغة Elixir تعلم X في Y دقيقة، حيث X = Elixir وثائق LiveCodes تحميل قالب البداية "
+ },
+ "elixirWasm.name": {
+ "translation": "Elixir (Wasm)"
+ },
"elm.desc": {
"translation": "يتم ترجمة لغة Elm إلى JavaScript في المتصفح بواسطة إصدار WebAssembly من مُترجم Elm."
},
diff --git a/src/livecodes/i18n/locales/ar/language-info.ts b/src/livecodes/i18n/locales/ar/language-info.ts
index b5d6df9d7..a1ab2054d 100644
--- a/src/livecodes/i18n/locales/ar/language-info.ts
+++ b/src/livecodes/i18n/locales/ar/language-info.ts
@@ -144,6 +144,11 @@ const languageInfo: I18nLangInfoTranslation = {
link: '<1><2>الموقع الرسمي2>1> <3> <4>وثائق LiveCodes4> 3>',
name: 'EJS',
},
+ elixirWasm: {
+ desc: 'مترجم Elixir يعمل على WebAssembly، باستخدام <1>AtomVM1>',
+ link: '<1> <2>موقع Elixir2> 1> <3> <4>وثائق لغة Elixir4> 3> <5> <6>تعلم X في Y دقيقة، حيث X = Elixir6> 5> <7> <8>وثائق LiveCodes8> 7> <9> <10>تحميل قالب البداية10> 9>',
+ name: 'Elixir (Wasm)',
+ },
elm: {
desc: 'يتم ترجمة لغة Elm إلى JavaScript في المتصفح بواسطة إصدار WebAssembly من مُترجم Elm.',
link: '<1><2>الموقع الرسمي لـ Elm2>1> <3><4>دليل Elm4>3> <5> <6>وثائق LiveCodes6> 5> <7> <8>تحميل قالب البداية8> 7>',
diff --git a/src/livecodes/i18n/locales/ar/translation.lokalise.json b/src/livecodes/i18n/locales/ar/translation.lokalise.json
index cebdf92f2..cb6cf1ed5 100644
--- a/src/livecodes/i18n/locales/ar/translation.lokalise.json
+++ b/src/livecodes/i18n/locales/ar/translation.lokalise.json
@@ -1992,6 +1992,9 @@
"templates.starter.duckdb": {
"translation": "قالب DuckDB"
},
+ "templates.starter.elixir-wasm": {
+ "translation": "قالب Elixir"
+ },
"templates.starter.elm": {
"translation": "قالب Elm"
},
diff --git a/src/livecodes/i18n/locales/ar/translation.ts b/src/livecodes/i18n/locales/ar/translation.ts
index 51e88ad44..5061b2c5a 100644
--- a/src/livecodes/i18n/locales/ar/translation.ts
+++ b/src/livecodes/i18n/locales/ar/translation.ts
@@ -1002,6 +1002,7 @@ const translation: I18nTranslation = {
daisyui: 'قالب daisyUI',
dart: 'قالب Dart',
diagrams: 'قالب الرسوم البيانية',
+ 'elixir-wasm': 'قالب Elixir',
elm: 'قالب Elm',
fanak: 'قالب Fanak',
fennel: 'قالب Fennel',
diff --git a/src/livecodes/i18n/locales/bn/language-info.lokalise.json b/src/livecodes/i18n/locales/bn/language-info.lokalise.json
index dd346eb35..486601d0e 100644
--- a/src/livecodes/i18n/locales/bn/language-info.lokalise.json
+++ b/src/livecodes/i18n/locales/bn/language-info.lokalise.json
@@ -261,6 +261,15 @@
"ejs.name": {
"translation": "EJS"
},
+ "elixirWasm.desc": {
+ "translation": "Elixir কম্পাইলার ওয়েবঅ্যাসেম্বলিতে চলছে,
AtomVM ব্যবহার করে"
+ },
+ "elixirWasm.link": {
+ "translation": "
Elixir ওয়েবসাইট Elixir ল্যাঙ্গুয়েজ ডকুমেন্টেশন Y মিনিটে X শিখুন, যেখানে X=Elixir LiveCodes ডকুমেন্টেশন স্টার্টার টেমপ্লেট লোড করুন "
+ },
+ "elixirWasm.name": {
+ "translation": "Elixir (Wasm)"
+ },
"elm.desc": {
"translation": "Elm কম্পাইলারের একটি WebAssembly বিল্ড দ্বারা ব্রাউজারে JavaScript-এ কম্পাইল করা হয়।"
},
diff --git a/src/livecodes/i18n/locales/bn/language-info.ts b/src/livecodes/i18n/locales/bn/language-info.ts
index 80c8a2cef..a56b76f02 100644
--- a/src/livecodes/i18n/locales/bn/language-info.ts
+++ b/src/livecodes/i18n/locales/bn/language-info.ts
@@ -144,6 +144,11 @@ const languageInfo: I18nLangInfoTranslation = {
link: '<1><2>অফিসিয়াল ওয়েবসাইট2>1> <3> <4>LiveCodes ডকুমেন্টেশন4> 3>',
name: 'EJS',
},
+ elixirWasm: {
+ desc: 'Elixir কম্পাইলার ওয়েবঅ্যাসেম্বলিতে চলছে, <1>AtomVM1> ব্যবহার করে',
+ link: '<1> <2>Elixir ওয়েবসাইট2> 1> <3> <4>Elixir ল্যাঙ্গুয়েজ ডকুমেন্টেশন4> 3> <5> <6>Y মিনিটে X শিখুন, যেখানে X=Elixir6> 5> <7> <8>LiveCodes ডকুমেন্টেশন8> 7> <9> <10>স্টার্টার টেমপ্লেট লোড করুন10> 9>',
+ name: 'Elixir (Wasm)',
+ },
elm: {
desc: 'Elm কম্পাইলারের একটি WebAssembly বিল্ড দ্বারা ব্রাউজারে JavaScript-এ কম্পাইল করা হয়।',
link: '<1><2>Elm অফিসিয়াল ওয়েবসাইট2>1> <3><4>Elm গাইড4>3> <5> <6>LiveCodes ডকুমেন্টেশন6> 5> <7> <8>স্টার্টার টেমপ্লেট লোড করুন8> 7>',
diff --git a/src/livecodes/i18n/locales/bn/translation.lokalise.json b/src/livecodes/i18n/locales/bn/translation.lokalise.json
index 89e60aa0b..002d820cb 100644
--- a/src/livecodes/i18n/locales/bn/translation.lokalise.json
+++ b/src/livecodes/i18n/locales/bn/translation.lokalise.json
@@ -1992,6 +1992,9 @@
"templates.starter.duckdb": {
"translation": "DuckDB স্টার্টার"
},
+ "templates.starter.elixir-wasm": {
+ "translation": "Elixir স্টার্টার"
+ },
"templates.starter.elm": {
"translation": "Elm স্টার্টার"
},
diff --git a/src/livecodes/i18n/locales/bn/translation.ts b/src/livecodes/i18n/locales/bn/translation.ts
index 8bdb306f9..b3e228a02 100644
--- a/src/livecodes/i18n/locales/bn/translation.ts
+++ b/src/livecodes/i18n/locales/bn/translation.ts
@@ -1002,6 +1002,7 @@ const translation: I18nTranslation = {
daisyui: 'daisyUI স্টার্টার',
dart: 'Dart স্টার্টার',
diagrams: 'Diagrams স্টার্টার',
+ 'elixir-wasm': 'Elixir স্টার্টার',
elm: 'Elm স্টার্টার',
fanak: 'Fanak স্টার্টার',
fennel: 'Fennel স্টার্টার',
diff --git a/src/livecodes/i18n/locales/de/language-info.lokalise.json b/src/livecodes/i18n/locales/de/language-info.lokalise.json
index 2a9e4436e..b1479742b 100644
--- a/src/livecodes/i18n/locales/de/language-info.lokalise.json
+++ b/src/livecodes/i18n/locales/de/language-info.lokalise.json
@@ -261,6 +261,15 @@
"ejs.name": {
"translation": "EJS"
},
+ "elixirWasm.desc": {
+ "translation": "Elixir-Compiler, der auf WebAssembly läuft, unter Verwendung von
AtomVM"
+ },
+ "elixirWasm.link": {
+ "translation": "
Elixir-Website Elixir-Sprachdokumentation Lerne X in Y Minuten, wobei X = Elixir LiveCodes-Dokumentation Starter-Elixirorlage laden "
+ },
+ "elixirWasm.name": {
+ "translation": "Elixir (Wasm)"
+ },
"elm.desc": {
"translation": "Elm wird im Browser durch eine WebAssembly-Version des Elm-Compilers in JavaScript kompiliert."
},
diff --git a/src/livecodes/i18n/locales/de/language-info.ts b/src/livecodes/i18n/locales/de/language-info.ts
index a97383fb5..9fb1b6f07 100644
--- a/src/livecodes/i18n/locales/de/language-info.ts
+++ b/src/livecodes/i18n/locales/de/language-info.ts
@@ -146,6 +146,11 @@ const languageInfo: I18nLangInfoTranslation = {
link: '<1><2>Offizielle Website2>1> <3> <4>LiveCodes Dokumentation4> 3>',
name: 'EJS',
},
+ elixirWasm: {
+ desc: 'Elixir-Compiler, der auf WebAssembly läuft, unter Verwendung von <1>AtomVM1>',
+ link: '<1> <2>Elixir-Website2> 1> <3> <4>Elixir-Sprachdokumentation4> 3> <5> <6>Lerne X in Y Minuten, wobei X = Elixir6> 5> <7> <8>LiveCodes-Dokumentation8> 7> <9> <10>Starter-Elixirorlage laden10> 9>',
+ name: 'Elixir (Wasm)',
+ },
elm: {
desc: 'Elm wird im Browser durch eine WebAssembly-Version des Elm-Compilers in JavaScript kompiliert.',
link: '<1><2>Offizielle Website von Elm2>1> <3><4>Elm-Handbuch4>3> <5> <6>LiveCodes-Dokumentation6> 5> <7> <8>Starter-Vorlage laden8> 7>',
diff --git a/src/livecodes/i18n/locales/de/translation.lokalise.json b/src/livecodes/i18n/locales/de/translation.lokalise.json
index 8bfbf0f9b..3cfc13a4a 100644
--- a/src/livecodes/i18n/locales/de/translation.lokalise.json
+++ b/src/livecodes/i18n/locales/de/translation.lokalise.json
@@ -1992,6 +1992,9 @@
"templates.starter.duckdb": {
"translation": "DuckDB-Starter"
},
+ "templates.starter.elixir-wasm": {
+ "translation": "Elixir-Starter"
+ },
"templates.starter.elm": {
"translation": "Elm-Starter"
},
diff --git a/src/livecodes/i18n/locales/de/translation.ts b/src/livecodes/i18n/locales/de/translation.ts
index 12bdfaafb..340a08fd6 100644
--- a/src/livecodes/i18n/locales/de/translation.ts
+++ b/src/livecodes/i18n/locales/de/translation.ts
@@ -1004,6 +1004,7 @@ const translation: I18nTranslation = {
daisyui: 'daisyUI-Starter',
dart: 'Dart-Starter',
diagrams: 'Diagramm-Starter',
+ 'elixir-wasm': 'Elixir-Starter',
elm: 'Elm-Starter',
fanak: 'Fanak-Starter',
fennel: 'Fennel-Starter',
diff --git a/src/livecodes/i18n/locales/en/language-info.lokalise.json b/src/livecodes/i18n/locales/en/language-info.lokalise.json
index 19e518dab..19e56bfca 100644
--- a/src/livecodes/i18n/locales/en/language-info.lokalise.json
+++ b/src/livecodes/i18n/locales/en/language-info.lokalise.json
@@ -348,6 +348,18 @@
"notes": "",
"translation": "EJS"
},
+ "elixirWasm.desc": {
+ "notes": "###
###\n\n\n",
+ "translation": "Elixir compiler running on WebAssembly, using AtomVM"
+ },
+ "elixirWasm.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": " Elixir official website Elixir documentation Learn X in Y minutes, where X=Elixir LiveCodes Documentation Load starter template "
+ },
+ "elixirWasm.name": {
+ "notes": "",
+ "translation": "Elixir (Wasm)"
+ },
"elm.desc": {
"notes": "",
"translation": "Elm is compiled to JavaScript in the browser by a WebAssembly build of the Elm compiler."
@@ -360,6 +372,18 @@
"notes": "",
"translation": "Elm"
},
+ "erlangWasm.desc": {
+ "notes": "### ###\n\n\n",
+ "translation": "Erlang runtime running on WebAssembly, using AtomVM"
+ },
+ "erlangWasm.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": " Erlang official website Erlang documentation Learn X in Y minutes, where X=Erlang LiveCodes Documentation Load starter template "
+ },
+ "erlangWasm.name": {
+ "notes": "",
+ "translation": "Erlang (Wasm)"
+ },
"eta.desc": {
"notes": "",
"translation": "Embedded JS template engine for Node, Deno, and the browser. Lightweight, fast, and pluggable. Written in TypeScript."
diff --git a/src/livecodes/i18n/locales/en/language-info.ts b/src/livecodes/i18n/locales/en/language-info.ts
index 95f5bca6c..dcff774f2 100644
--- a/src/livecodes/i18n/locales/en/language-info.ts
+++ b/src/livecodes/i18n/locales/en/language-info.ts
@@ -157,11 +157,21 @@ const languageInfo = {
link: '<1><2>Official website2>1> <3> <4>LiveCodes Documentations4> 3>',
name: 'EJS',
},
+ elixirWasm: {
+ desc: 'Elixir compiler running on WebAssembly, using <1>AtomVM1>',
+ link: '<1> <2>Elixir official website2> 1> <3> <4>Elixir documentation4> 3> <5> <6>Learn X in Y minutes, where X=Elixir6> 5> <7> <8>LiveCodes Documentation8> 7> <9> <10>Load starter template10> 9>',
+ name: 'Elixir (Wasm)',
+ },
elm: {
desc: 'Elm is compiled to JavaScript in the browser by a WebAssembly build of the Elm compiler.',
link: '<1><2>Elm official website2>1> <3><4>Elm guide4>3> <5> <6>LiveCodes Documentation6> 5> <7> <8>Load starter template8> 7>',
name: 'Elm',
},
+ erlangWasm: {
+ desc: 'Erlang runtime running on WebAssembly, using <1>AtomVM1>',
+ link: '<1> <2>Erlang official website2> 1> <3> <4>Erlang documentation4> 3> <5> <6>Learn X in Y minutes, where X=Erlang6> 5> <7> <8>LiveCodes Documentation8> 7> <9> <10>Load starter template10> 9>',
+ name: 'Erlang (Wasm)',
+ },
eta: {
desc: 'Embedded JS template engine for Node, Deno, and the browser. Lightweight, fast, and pluggable. Written in TypeScript.',
link: '<1><2>Official website2>1> <3> <4>Documentation4> 3> <5> <6>LiveCodes Documentations6> 5>',
diff --git a/src/livecodes/i18n/locales/en/translation.lokalise.json b/src/livecodes/i18n/locales/en/translation.lokalise.json
index e0beaf8f2..2c92bfdec 100644
--- a/src/livecodes/i18n/locales/en/translation.lokalise.json
+++ b/src/livecodes/i18n/locales/en/translation.lokalise.json
@@ -2656,10 +2656,18 @@
"notes": "",
"translation": "DuckDB Starter"
},
+ "templates.starter.elixir-wasm": {
+ "notes": "",
+ "translation": "Elixir Starter"
+ },
"templates.starter.elm": {
"notes": "",
"translation": "Elm Starter"
},
+ "templates.starter.erlang-wasm": {
+ "notes": "",
+ "translation": "Erlang Starter"
+ },
"templates.starter.fanak": {
"notes": "",
"translation": "Fanak Starter"
diff --git a/src/livecodes/i18n/locales/en/translation.ts b/src/livecodes/i18n/locales/en/translation.ts
index 83e37810f..dd3ef7cd2 100644
--- a/src/livecodes/i18n/locales/en/translation.ts
+++ b/src/livecodes/i18n/locales/en/translation.ts
@@ -1010,7 +1010,9 @@ const translation = {
dart: 'Dart Starter',
diagrams: 'Diagrams Starter',
duckdb: 'DuckDB Starter',
+ 'elixir-wasm': 'Elixir Starter',
elm: 'Elm Starter',
+ 'erlang-wasm': 'Erlang Starter',
fanak: 'Fanak Starter',
fennel: 'Fennel Starter',
flutter: 'Flutter Starter',
diff --git a/src/livecodes/i18n/locales/es/language-info.lokalise.json b/src/livecodes/i18n/locales/es/language-info.lokalise.json
index a335af022..b93152d00 100644
--- a/src/livecodes/i18n/locales/es/language-info.lokalise.json
+++ b/src/livecodes/i18n/locales/es/language-info.lokalise.json
@@ -261,6 +261,15 @@
"ejs.name": {
"translation": "EJS"
},
+ "elixirWasm.desc": {
+ "translation": "Compilador Elixir que se ejecuta en WebAssembly, utilizando AtomVM"
+ },
+ "elixirWasm.link": {
+ "translation": " Página web de Elixir Documentación del lenguaje Elixir Aprende X en Y minutos, donde X = Elixir Documentación de LiveCodes Cargar plantilla de inicio "
+ },
+ "elixirWasm.name": {
+ "translation": "Elixir (Wasm)"
+ },
"elm.desc": {
"translation": "Elm se compila a JavaScript en el navegador mediante una versión de WebAssembly del compilador de Elm."
},
diff --git a/src/livecodes/i18n/locales/es/language-info.ts b/src/livecodes/i18n/locales/es/language-info.ts
index da4d39fb3..a905e91bb 100644
--- a/src/livecodes/i18n/locales/es/language-info.ts
+++ b/src/livecodes/i18n/locales/es/language-info.ts
@@ -145,6 +145,11 @@ const languageInfo: I18nLangInfoTranslation = {
link: '<1><2>Sitio web oficial2>1> <3> <4>Documentación de LiveCodes4> 3>',
name: 'EJS',
},
+ elixirWasm: {
+ desc: 'Compilador Elixir que se ejecuta en WebAssembly, utilizando <1>AtomVM1>',
+ link: '<1> <2>Página web de Elixir2> 1> <3> <4>Documentación del lenguaje Elixir4> 3> <5> <6>Aprende X en Y minutos, donde X = Elixir6> 5> <7> <8>Documentación de LiveCodes8> 7> <9> <10>Cargar plantilla de inicio10> 9>',
+ name: 'Elixir (Wasm)',
+ },
elm: {
desc: 'Elm se compila a JavaScript en el navegador mediante una versión de WebAssembly del compilador de Elm.',
link: '<1><2>Página web oficial de Elm2>1> <3><4>Guía de Elm4>3> <5> <6>Documentación de LiveCodes6> 5> <7> <8>Cargar plantilla de inicio8> 7>',
diff --git a/src/livecodes/i18n/locales/es/translation.lokalise.json b/src/livecodes/i18n/locales/es/translation.lokalise.json
index bea02ee7c..555f849a3 100644
--- a/src/livecodes/i18n/locales/es/translation.lokalise.json
+++ b/src/livecodes/i18n/locales/es/translation.lokalise.json
@@ -1992,6 +1992,9 @@
"templates.starter.duckdb": {
"translation": "Inicio DuckDB"
},
+ "templates.starter.elixir-wasm": {
+ "translation": "Inicio Elixir"
+ },
"templates.starter.elm": {
"translation": "Inicio Elm"
},
diff --git a/src/livecodes/i18n/locales/es/translation.ts b/src/livecodes/i18n/locales/es/translation.ts
index 31fe2c642..b2e73f1c5 100644
--- a/src/livecodes/i18n/locales/es/translation.ts
+++ b/src/livecodes/i18n/locales/es/translation.ts
@@ -1002,6 +1002,7 @@ const translation: I18nTranslation = {
daisyui: 'Inicio daisyUI',
dart: 'Inicio Dart',
diagrams: 'Inicio Diagramas',
+ 'elixir-wasm': 'Inicio Elixir',
elm: 'Inicio Elm',
fanak: 'Inicio Fanak',
fennel: 'Inicio Fennel',
diff --git a/src/livecodes/i18n/locales/fa/language-info.lokalise.json b/src/livecodes/i18n/locales/fa/language-info.lokalise.json
index 438f25970..b9a0b9d0e 100644
--- a/src/livecodes/i18n/locales/fa/language-info.lokalise.json
+++ b/src/livecodes/i18n/locales/fa/language-info.lokalise.json
@@ -261,6 +261,15 @@
"ejs.name": {
"translation": "EJS"
},
+ "elixirWasm.desc": {
+ "translation": "کامپایلر Elixir که روی WebAssembly اجرا میشود، با استفاده از AtomVM"
+ },
+ "elixirWasm.link": {
+ "translation": " وبسایت Elixir مستندات زبان Elixir زبان Elixir را در چند دقیقه بیاموزید مستندات LiveCodes بارگذاری قالب شروع "
+ },
+ "elixirWasm.name": {
+ "translation": "Elixir (Wasm)"
+ },
"elm.desc": {
"translation": "زبان Elm توسط یک نسخه WebAssembly از کامپایلر Elm، در مرورگر به جاوااسکریپت کامپایل میشود."
},
diff --git a/src/livecodes/i18n/locales/fa/language-info.ts b/src/livecodes/i18n/locales/fa/language-info.ts
index 87d5b755b..75dfc3089 100644
--- a/src/livecodes/i18n/locales/fa/language-info.ts
+++ b/src/livecodes/i18n/locales/fa/language-info.ts
@@ -145,6 +145,11 @@ const languageInfo: I18nLangInfoTranslation = {
link: '<1><2>وبسایت رسمی2>1> <3> <4>مستندات LiveCodes4> 3>',
name: 'EJS',
},
+ elixirWasm: {
+ desc: 'کامپایلر Elixir که روی WebAssembly اجرا میشود، با استفاده از <1>AtomVM1>',
+ link: '<1> <2>وبسایت Elixir2> 1> <3> <4>مستندات زبان Elixir4> 3> <5> <6>زبان Elixir را در چند دقیقه بیاموزید6> 5> <7> <8>مستندات LiveCodes8> 7> <9> <10>بارگذاری قالب شروع10> 9>',
+ name: 'Elixir (Wasm)',
+ },
elm: {
desc: 'زبان Elm توسط یک نسخه WebAssembly از کامپایلر Elm، در مرورگر به جاوااسکریپت کامپایل میشود.',
link: '<1><2>وبسایت رسمی Elm2>1> <3><4>راهنمای Elm4>3> <5> <6>مستندات LiveCodes6> 5> <7> <8>بارگذاری الگوی اولیه8> 7>',
diff --git a/src/livecodes/i18n/locales/fa/translation.lokalise.json b/src/livecodes/i18n/locales/fa/translation.lokalise.json
index 1235af113..c8b8bde19 100644
--- a/src/livecodes/i18n/locales/fa/translation.lokalise.json
+++ b/src/livecodes/i18n/locales/fa/translation.lokalise.json
@@ -1992,6 +1992,9 @@
"templates.starter.duckdb": {
"translation": "شروع کننده DuckDB"
},
+ "templates.starter.elixir-wasm": {
+ "translation": "شروع کننده Elixir"
+ },
"templates.starter.elm": {
"translation": "شروع کننده Elm"
},
diff --git a/src/livecodes/i18n/locales/fa/translation.ts b/src/livecodes/i18n/locales/fa/translation.ts
index a6e426f02..4c61d8610 100644
--- a/src/livecodes/i18n/locales/fa/translation.ts
+++ b/src/livecodes/i18n/locales/fa/translation.ts
@@ -1003,6 +1003,7 @@ const translation: I18nTranslation = {
daisyui: 'شروع کننده daisyUI',
dart: 'شروع کننده Dart',
diagrams: 'شروع کننده نمودارها',
+ 'elixir-wasm': 'شروع کننده Elixir',
elm: 'شروع کننده Elm',
fanak: 'شروع کننده Fanak',
fennel: 'شروع کننده Fennel',
diff --git a/src/livecodes/i18n/locales/fr/language-info.lokalise.json b/src/livecodes/i18n/locales/fr/language-info.lokalise.json
index 9f4d2e8bb..4ddf38174 100644
--- a/src/livecodes/i18n/locales/fr/language-info.lokalise.json
+++ b/src/livecodes/i18n/locales/fr/language-info.lokalise.json
@@ -261,6 +261,15 @@
"ejs.name": {
"translation": "EJS"
},
+ "elixirWasm.desc": {
+ "translation": "Compilateur Elixir fonctionnant sur WebAssembly, utilisant AtomVM"
+ },
+ "elixirWasm.link": {
+ "translation": " Site web d'Elixir Documentation du langage Elixir Apprenez X en Y minutes, où X = Elixir Documentation LiveCodes Charger un modèle de démarrage "
+ },
+ "elixirWasm.name": {
+ "translation": "Elixir (Wasm)"
+ },
"elm.desc": {
"translation": "Elm est compilé en JavaScript dans le navigateur grâce à une version WebAssembly du compilateur Elm."
},
diff --git a/src/livecodes/i18n/locales/fr/language-info.ts b/src/livecodes/i18n/locales/fr/language-info.ts
index efe967ae4..f00ca84fa 100644
--- a/src/livecodes/i18n/locales/fr/language-info.ts
+++ b/src/livecodes/i18n/locales/fr/language-info.ts
@@ -145,6 +145,11 @@ const languageInfo: I18nLangInfoTranslation = {
link: '<1><2>Site officiel2>1> <3> <4>Documentation LiveCodes4> 3>',
name: 'EJS',
},
+ elixirWasm: {
+ desc: 'Compilateur Elixir fonctionnant sur WebAssembly, utilisant <1>AtomVM1>',
+ link: "<1> <2>Site web d'Elixir2> 1> <3> <4>Documentation du langage Elixir4> 3> <5> <6>Apprenez X en Y minutes, où X = Elixir6> 5> <7> <8>Documentation LiveCodes8> 7> <9> <10>Charger un modèle de démarrage10> 9>",
+ name: 'Elixir (Wasm)',
+ },
elm: {
desc: 'Elm est compilé en JavaScript dans le navigateur grâce à une version WebAssembly du compilateur Elm.',
link: "<1><2>Site officiel d'Elm2>1> <3><4>Guide d'Elm4>3> <5> <6>Documentation LiveCodes6> 5> <7> <8>Charger un modèle de démarrage8> 7>",
diff --git a/src/livecodes/i18n/locales/fr/translation.lokalise.json b/src/livecodes/i18n/locales/fr/translation.lokalise.json
index cbd869241..2cd5c4022 100644
--- a/src/livecodes/i18n/locales/fr/translation.lokalise.json
+++ b/src/livecodes/i18n/locales/fr/translation.lokalise.json
@@ -1992,6 +1992,9 @@
"templates.starter.duckdb": {
"translation": "Démarrage DuckDB"
},
+ "templates.starter.elixir-wasm": {
+ "translation": "Démarrage Elixir"
+ },
"templates.starter.elm": {
"translation": "Démarrage Elm"
},
diff --git a/src/livecodes/i18n/locales/fr/translation.ts b/src/livecodes/i18n/locales/fr/translation.ts
index 68bc9e254..ead85e3eb 100644
--- a/src/livecodes/i18n/locales/fr/translation.ts
+++ b/src/livecodes/i18n/locales/fr/translation.ts
@@ -1004,6 +1004,7 @@ const translation: I18nTranslation = {
daisyui: 'Démarrage daisyUI',
dart: 'Démarrage Dart',
diagrams: 'Démarrage Diagrammes',
+ 'elixir-wasm': 'Démarrage Elixir',
elm: 'Démarrage Elm',
fanak: 'Démarrage Fanak',
fennel: 'Démarrage Fennel',
diff --git a/src/livecodes/i18n/locales/hi/language-info.lokalise.json b/src/livecodes/i18n/locales/hi/language-info.lokalise.json
index 38b80310f..5031efeee 100644
--- a/src/livecodes/i18n/locales/hi/language-info.lokalise.json
+++ b/src/livecodes/i18n/locales/hi/language-info.lokalise.json
@@ -261,6 +261,15 @@
"ejs.name": {
"translation": "EJS"
},
+ "elixirWasm.desc": {
+ "translation": "वेबअसेंबली पर चल रहा Elixir कंपाइलर, AtomVM का उपयोग करके"
+ },
+ "elixirWasm.link": {
+ "translation": " Elixir वेबसाइट Elixir भाषा दस्तावेज़ीकरण वाई मिनटों में एक्स सीखें, जहां एक्स=Elixir लाइवकोड दस्तावेज़ीकरण स्टार्टर टेम्पलेट लोड करें "
+ },
+ "elixirWasm.name": {
+ "translation": "Elixir (Wasm)"
+ },
"elm.desc": {
"translation": "एल्म को ब्राउज़र में एल्म कंपाइलर के एक वेबअसेंबली (WebAssembly) बिल्ड द्वारा जावास्क्रिप्ट में कंपाइल किया जाता है।"
},
diff --git a/src/livecodes/i18n/locales/hi/language-info.ts b/src/livecodes/i18n/locales/hi/language-info.ts
index a1ba86c44..e8b4d8553 100644
--- a/src/livecodes/i18n/locales/hi/language-info.ts
+++ b/src/livecodes/i18n/locales/hi/language-info.ts
@@ -145,6 +145,11 @@ const languageInfo: I18nLangInfoTranslation = {
link: '<1><2>आधिकारिक वेबसाइट2>1> <3> <4>LiveCodes दस्तावेज़ीकरण4> 3>',
name: 'EJS',
},
+ elixirWasm: {
+ desc: 'वेबअसेंबली पर चल रहा Elixir कंपाइलर, <1>AtomVM1> का उपयोग करके',
+ link: '<1> <2>Elixir वेबसाइट2> 1> <3> <4>Elixir भाषा दस्तावेज़ीकरण4> 3> <5> <6>वाई मिनटों में एक्स सीखें, जहां एक्स=Elixir6> 5> <7> <8>लाइवकोड दस्तावेज़ीकरण8> 7> <9> <10>स्टार्टर टेम्पलेट लोड करें10> 9>',
+ name: 'Elixir (Wasm)',
+ },
elm: {
desc: 'एल्म को ब्राउज़र में एल्म कंपाइलर के एक वेबअसेंबली (WebAssembly) बिल्ड द्वारा जावास्क्रिप्ट में कंपाइल किया जाता है।',
link: '<1><2>Elm आधिकारिक वेबसाइट2>1> <3><4>Elm गाइड4>3> <5> <6>LiveCodes दस्तावेज़ीकरण6> 5> <7> <8>स्टार्टर टेम्पलेट लोड करें8> 7>',
diff --git a/src/livecodes/i18n/locales/hi/translation.lokalise.json b/src/livecodes/i18n/locales/hi/translation.lokalise.json
index a92a6f646..da8809531 100644
--- a/src/livecodes/i18n/locales/hi/translation.lokalise.json
+++ b/src/livecodes/i18n/locales/hi/translation.lokalise.json
@@ -1992,6 +1992,9 @@
"templates.starter.duckdb": {
"translation": "DuckDB स्टार्टर"
},
+ "templates.starter.elixir-wasm": {
+ "translation": "Elixir स्टार्टर"
+ },
"templates.starter.elm": {
"translation": "Elm स्टार्टर"
},
diff --git a/src/livecodes/i18n/locales/hi/translation.ts b/src/livecodes/i18n/locales/hi/translation.ts
index cb7bccbd4..5a437e11e 100644
--- a/src/livecodes/i18n/locales/hi/translation.ts
+++ b/src/livecodes/i18n/locales/hi/translation.ts
@@ -1002,6 +1002,7 @@ const translation: I18nTranslation = {
daisyui: 'daisyUI स्टार्टर',
dart: 'Dart स्टार्टर',
diagrams: 'आरेख स्टार्टर',
+ 'elixir-wasm': 'Elixir स्टार्टर',
elm: 'Elm स्टार्टर',
fanak: 'Fanak स्टार्टर',
fennel: 'Fennel स्टार्टर',
diff --git a/src/livecodes/i18n/locales/hu/language-info.lokalise.json b/src/livecodes/i18n/locales/hu/language-info.lokalise.json
index f529f99b2..0cf01a463 100644
--- a/src/livecodes/i18n/locales/hu/language-info.lokalise.json
+++ b/src/livecodes/i18n/locales/hu/language-info.lokalise.json
@@ -261,6 +261,15 @@
"ejs.name": {
"translation": "EJS"
},
+ "elixirWasm.desc": {
+ "translation": "Az Elixir fordító WebAssembly-en fut, a AtomVM használatával"
+ },
+ "elixirWasm.link": {
+ "translation": " Az Elixir weboldala Az Elixir nyelv dokumentációja Tanulj meg X-et Y perc alatt, ahol X=Elixir LiveCodes dokumentáció Indító sablon betöltése "
+ },
+ "elixirWasm.name": {
+ "translation": "Elixir (Wasm)"
+ },
"elm.desc": {
"translation": "Az Elm-et a böngészőben az Elm-fordító WebAssembly-verziója fordítja le JavaScript-re."
},
diff --git a/src/livecodes/i18n/locales/hu/language-info.ts b/src/livecodes/i18n/locales/hu/language-info.ts
index d4ecc7148..937852a4e 100644
--- a/src/livecodes/i18n/locales/hu/language-info.ts
+++ b/src/livecodes/i18n/locales/hu/language-info.ts
@@ -146,6 +146,11 @@ const languageInfo: I18nLangInfoTranslation = {
link: '<1><2>Hivatalos weboldal2>1> <3> <4>LiveCodes dokumentáció4> 3>',
name: 'EJS',
},
+ elixirWasm: {
+ desc: 'Az Elixir fordító WebAssembly-en fut, a <1>AtomVM1> használatával',
+ link: '<1> <2>Az Elixir weboldala2> 1> <3> <4>Az Elixir nyelv dokumentációja4> 3> <5> <6>Tanulj meg X-et Y perc alatt, ahol X=Elixir6> 5> <7> <8>LiveCodes dokumentáció8> 7> <9> <10>Indító sablon betöltése10> 9>',
+ name: 'Elixir (Wasm)',
+ },
elm: {
desc: 'Az Elm-et a böngészőben az Elm-fordító WebAssembly-verziója fordítja le JavaScript-re.',
link: '<1><2>Az Elm hivatalos weboldala2>1> <3><4>Elm útmutató4>3> <5> <6>LiveCodes dokumentáció6> 5> <7> <8>Kezdő sablon betöltése8> 7>',
diff --git a/src/livecodes/i18n/locales/hu/translation.lokalise.json b/src/livecodes/i18n/locales/hu/translation.lokalise.json
index 2a9352303..784514579 100644
--- a/src/livecodes/i18n/locales/hu/translation.lokalise.json
+++ b/src/livecodes/i18n/locales/hu/translation.lokalise.json
@@ -1992,6 +1992,9 @@
"templates.starter.duckdb": {
"translation": "DuckDB kezdő"
},
+ "templates.starter.elixir-wasm": {
+ "translation": "Elixir kezdő"
+ },
"templates.starter.elm": {
"translation": "Elm kezdő"
},
diff --git a/src/livecodes/i18n/locales/hu/translation.ts b/src/livecodes/i18n/locales/hu/translation.ts
index 5aa168ffe..719f9548f 100644
--- a/src/livecodes/i18n/locales/hu/translation.ts
+++ b/src/livecodes/i18n/locales/hu/translation.ts
@@ -1003,6 +1003,7 @@ const translation: I18nTranslation = {
daisyui: 'daisyUI kezdő',
dart: 'Dart kezdő',
diagrams: 'Diagramok kezdő',
+ 'elixir-wasm': 'Elixir kezdő',
elm: 'Elm kezdő',
fanak: 'Fanak kezdő',
fennel: 'Fennel kezdő',
diff --git a/src/livecodes/i18n/locales/id/language-info.lokalise.json b/src/livecodes/i18n/locales/id/language-info.lokalise.json
index b468e6269..5fdf7feb4 100644
--- a/src/livecodes/i18n/locales/id/language-info.lokalise.json
+++ b/src/livecodes/i18n/locales/id/language-info.lokalise.json
@@ -261,6 +261,15 @@
"ejs.name": {
"translation": "EJS"
},
+ "elixirWasm.desc": {
+ "translation": "Kompiler Elixir yang berjalan di WebAssembly, menggunakan AtomVM"
+ },
+ "elixirWasm.link": {
+ "translation": " Situs web Elixir Dokumentasi bahasa Elixir Pelajari X dalam Y menit, di mana X = Elixir Dokumentasi LiveCodes Muat templat awal "
+ },
+ "elixirWasm.name": {
+ "translation": "Elixir (Wasm)"
+ },
"elm.desc": {
"translation": "Elm dikompilasi menjadi JavaScript di dalam browser melalui versi WebAssembly dari kompiler Elm."
},
diff --git a/src/livecodes/i18n/locales/id/language-info.ts b/src/livecodes/i18n/locales/id/language-info.ts
index 0c4c488a2..84a9325d8 100644
--- a/src/livecodes/i18n/locales/id/language-info.ts
+++ b/src/livecodes/i18n/locales/id/language-info.ts
@@ -146,6 +146,11 @@ const languageInfo: I18nLangInfoTranslation = {
link: '<1><2>Situs web resmi2>1> <3> <4>Dokumentasi LiveCodes4> 3>',
name: 'EJS',
},
+ elixirWasm: {
+ desc: 'Kompiler Elixir yang berjalan di WebAssembly, menggunakan <1>AtomVM1>',
+ link: '<1> <2>Situs web Elixir2> 1> <3> <4>Dokumentasi bahasa Elixir4> 3> <5> <6>Pelajari X dalam Y menit, di mana X = Elixir6> 5> <7> <8>Dokumentasi LiveCodes8> 7> <9> <10>Muat templat awal10> 9>',
+ name: 'Elixir (Wasm)',
+ },
elm: {
desc: 'Elm dikompilasi menjadi JavaScript di dalam browser melalui versi WebAssembly dari kompiler Elm.',
link: '<1><2>Situs web resmi Elm2>1> <3><4>Panduan Elm4>3> <5> <6>Dokumentasi LiveCodes6> 5> <7> <8>Muat templat awal8> 7>',
diff --git a/src/livecodes/i18n/locales/id/translation.lokalise.json b/src/livecodes/i18n/locales/id/translation.lokalise.json
index 657a650ea..c0f4f5313 100644
--- a/src/livecodes/i18n/locales/id/translation.lokalise.json
+++ b/src/livecodes/i18n/locales/id/translation.lokalise.json
@@ -1992,6 +1992,9 @@
"templates.starter.duckdb": {
"translation": "Pemula DuckDB"
},
+ "templates.starter.elixir-wasm": {
+ "translation": "Pemula Elixir"
+ },
"templates.starter.elm": {
"translation": "Pemula Elm"
},
diff --git a/src/livecodes/i18n/locales/id/translation.ts b/src/livecodes/i18n/locales/id/translation.ts
index 1318ce53e..e6f74c52a 100644
--- a/src/livecodes/i18n/locales/id/translation.ts
+++ b/src/livecodes/i18n/locales/id/translation.ts
@@ -1002,6 +1002,7 @@ const translation: I18nTranslation = {
daisyui: 'Pemula daisyUI',
dart: 'Pemula Dart',
diagrams: 'Pemula Diagram',
+ 'elixir-wasm': 'Pemula Elixir',
elm: 'Pemula Elm',
fanak: 'Pemula Fanak',
fennel: 'Pemula Fennel',
diff --git a/src/livecodes/i18n/locales/it/language-info.lokalise.json b/src/livecodes/i18n/locales/it/language-info.lokalise.json
index c9b4ef01e..02b5d7eae 100644
--- a/src/livecodes/i18n/locales/it/language-info.lokalise.json
+++ b/src/livecodes/i18n/locales/it/language-info.lokalise.json
@@ -261,6 +261,15 @@
"ejs.name": {
"translation": "EJS"
},
+ "elixirWasm.desc": {
+ "translation": "Compilatore Elixir in esecuzione su WebAssembly, utilizzando AtomVM"
+ },
+ "elixirWasm.link": {
+ "translation": " Sito web di Elixir Documentazione sul linguaggio Elixir Impara X in Y minuti, dove X = Elixir Documentazione LiveCodes Carica il modello di avvio "
+ },
+ "elixirWasm.name": {
+ "translation": "Elixir (Wasm)"
+ },
"elm.desc": {
"translation": "Elm viene compilato in JavaScript nel browser tramite una versione WebAssembly del compilatore Elm."
},
diff --git a/src/livecodes/i18n/locales/it/language-info.ts b/src/livecodes/i18n/locales/it/language-info.ts
index 181e580f2..db450512b 100644
--- a/src/livecodes/i18n/locales/it/language-info.ts
+++ b/src/livecodes/i18n/locales/it/language-info.ts
@@ -145,6 +145,11 @@ const languageInfo: I18nLangInfoTranslation = {
link: '<1><2>Sito ufficiale2>1> <3> <4>Documentazione LiveCodes4> 3>',
name: 'EJS',
},
+ elixirWasm: {
+ desc: 'Compilatore Elixir in esecuzione su WebAssembly, utilizzando <1>AtomVM1>',
+ link: '<1> <2>Sito web di Elixir2> 1> <3> <4>Documentazione sul linguaggio Elixir4> 3> <5> <6>Impara X in Y minuti, dove X = Elixir6> 5> <7> <8>Documentazione LiveCodes8> 7> <9> <10>Carica il modello di avvio10> 9>',
+ name: 'Elixir (Wasm)',
+ },
elm: {
desc: 'Elm viene compilato in JavaScript nel browser tramite una versione WebAssembly del compilatore Elm.',
link: '<1><2>Sito web ufficiale di Elm2>1> <3><4>Guida a Elm4>3> <5> <6>Documentazione LiveCodes6> 5> <7> <8>Carica il modello iniziale8> 7>',
diff --git a/src/livecodes/i18n/locales/it/translation.lokalise.json b/src/livecodes/i18n/locales/it/translation.lokalise.json
index b2730128e..a7ff60c06 100644
--- a/src/livecodes/i18n/locales/it/translation.lokalise.json
+++ b/src/livecodes/i18n/locales/it/translation.lokalise.json
@@ -1992,6 +1992,9 @@
"templates.starter.duckdb": {
"translation": "Starter DuckDB"
},
+ "templates.starter.elixir-wasm": {
+ "translation": "Starter Elixir"
+ },
"templates.starter.elm": {
"translation": "Starter Elm"
},
diff --git a/src/livecodes/i18n/locales/it/translation.ts b/src/livecodes/i18n/locales/it/translation.ts
index da7c091f4..bd7548d38 100644
--- a/src/livecodes/i18n/locales/it/translation.ts
+++ b/src/livecodes/i18n/locales/it/translation.ts
@@ -1003,6 +1003,7 @@ const translation: I18nTranslation = {
daisyui: 'Starter daisyUI',
dart: 'Starter Dart',
diagrams: 'Starter Diagrammi',
+ 'elixir-wasm': 'Starter Elixir',
elm: 'Starter Elm',
fanak: 'Starter Fanak',
fennel: 'Starter Fennel',
diff --git a/src/livecodes/i18n/locales/ja/language-info.lokalise.json b/src/livecodes/i18n/locales/ja/language-info.lokalise.json
index 3018f9b0b..bc2683998 100644
--- a/src/livecodes/i18n/locales/ja/language-info.lokalise.json
+++ b/src/livecodes/i18n/locales/ja/language-info.lokalise.json
@@ -261,6 +261,15 @@
"ejs.name": {
"translation": "EJS"
},
+ "elixirWasm.desc": {
+ "translation": "WebAssembly上で動作するElixirコンパイラ。AtomVM を使用。"
+ },
+ "elixirWasm.link": {
+ "translation": " Elixirのウェブサイト Elixir言語のドキュメント 「XをY分で学ぶ」(X=Elixir) LiveCodes ドキュメント スターターテンプレートを読み込む "
+ },
+ "elixirWasm.name": {
+ "translation": "Elixir (Wasm)"
+ },
"elm.desc": {
"translation": "Elmは、ElmコンパイラのWebAssemblyビルドによって、ブラウザ上でJavaScriptにコンパイルされます。"
},
diff --git a/src/livecodes/i18n/locales/ja/language-info.ts b/src/livecodes/i18n/locales/ja/language-info.ts
index 3cd5030c2..763ac016b 100644
--- a/src/livecodes/i18n/locales/ja/language-info.ts
+++ b/src/livecodes/i18n/locales/ja/language-info.ts
@@ -145,6 +145,11 @@ const languageInfo: I18nLangInfoTranslation = {
link: '<1><2>公式ウェブサイト2>1> <3> <4>LiveCodes ドキュメント4> 3>',
name: 'EJS',
},
+ elixirWasm: {
+ desc: 'WebAssembly上で動作するElixirコンパイラ。<1>AtomVM を使用。1>',
+ link: '<1> <2>Elixirのウェブサイト2> 1> <3> <4>Elixir言語のドキュメント4> 3> <5> <6>「XをY分で学ぶ」(X=Elixir)6> 5> <7> <8>LiveCodes ドキュメント8> 7> <9> <10>スターターテンプレートを読み込む10> 9>',
+ name: 'Elixir (Wasm)',
+ },
elm: {
desc: 'Elmは、ElmコンパイラのWebAssemblyビルドによって、ブラウザ上でJavaScriptにコンパイルされます。',
link: '<1><2>Elm公式サイト2>1> <3><4>Elmガイド4>3> <5> <6>LiveCodes ドキュメント6> 5> <7> <8>スターターテンプレートの読み込み8> 7>',
diff --git a/src/livecodes/i18n/locales/ja/translation.lokalise.json b/src/livecodes/i18n/locales/ja/translation.lokalise.json
index 2a3830c8b..2ee7780a5 100644
--- a/src/livecodes/i18n/locales/ja/translation.lokalise.json
+++ b/src/livecodes/i18n/locales/ja/translation.lokalise.json
@@ -1992,6 +1992,9 @@
"templates.starter.duckdb": {
"translation": "DuckDBスターター"
},
+ "templates.starter.elixir-wasm": {
+ "translation": "Elixirスターター"
+ },
"templates.starter.elm": {
"translation": "Elmスターター"
},
diff --git a/src/livecodes/i18n/locales/ja/translation.ts b/src/livecodes/i18n/locales/ja/translation.ts
index 7db0a2128..9cabe9270 100644
--- a/src/livecodes/i18n/locales/ja/translation.ts
+++ b/src/livecodes/i18n/locales/ja/translation.ts
@@ -1003,6 +1003,7 @@ const translation: I18nTranslation = {
daisyui: 'daisyUIスターター',
dart: 'Dart スターター',
diagrams: 'ダイアグラムスターター',
+ 'elixir-wasm': 'Elixirスターター',
elm: 'Elmスターター',
fanak: 'Fanakスターター',
fennel: 'Fennelスターター',
diff --git a/src/livecodes/i18n/locales/nl/language-info.lokalise.json b/src/livecodes/i18n/locales/nl/language-info.lokalise.json
index 2ddda79bf..b25e0b5b4 100644
--- a/src/livecodes/i18n/locales/nl/language-info.lokalise.json
+++ b/src/livecodes/i18n/locales/nl/language-info.lokalise.json
@@ -261,6 +261,15 @@
"ejs.name": {
"translation": "EJS"
},
+ "elixirWasm.desc": {
+ "translation": "Elixir-compiler die op WebAssembly draait, met AtomVM"
+ },
+ "elixirWasm.link": {
+ "translation": " Elixir-website Documentatie over de taal Elixir Leer X in Y minuten, waarbij X = Elixir LiveCodes-documentatie Starterssjabloon laden "
+ },
+ "elixirWasm.name": {
+ "translation": "Elixir (Wasm)"
+ },
"elm.desc": {
"translation": "Elm wordt in de browser naar JavaScript gecompileerd door een WebAssembly-versie van de Elm-compiler."
},
diff --git a/src/livecodes/i18n/locales/nl/language-info.ts b/src/livecodes/i18n/locales/nl/language-info.ts
index 896f93fcb..749f472a8 100644
--- a/src/livecodes/i18n/locales/nl/language-info.ts
+++ b/src/livecodes/i18n/locales/nl/language-info.ts
@@ -145,6 +145,11 @@ const languageInfo: I18nLangInfoTranslation = {
link: '<1><2>Officiële website2>1> <3> <4>LiveCodes Documentatie4> 3>',
name: 'EJS',
},
+ elixirWasm: {
+ desc: 'Elixir-compiler die op WebAssembly draait, met <1>AtomVM1>',
+ link: '<1> <2>Elixir-website2> 1> <3> <4>Documentatie over de taal Elixir4> 3> <5> <6>Leer X in Y minuten, waarbij X = Elixir6> 5> <7> <8>LiveCodes-documentatie8> 7> <9> <10>Starterssjabloon laden10> 9>',
+ name: 'Elixir (Wasm)',
+ },
elm: {
desc: 'Elm wordt in de browser naar JavaScript gecompileerd door een WebAssembly-versie van de Elm-compiler.',
link: '<1><2>Officiële website van Elm2>1> <3><4>Elm-handleiding4>3> <5> <6>LiveCodes-documentatie6> 5> <7> <8>Startersjabloon laden8> 7>',
diff --git a/src/livecodes/i18n/locales/nl/translation.lokalise.json b/src/livecodes/i18n/locales/nl/translation.lokalise.json
index 92d014187..a4730e20a 100644
--- a/src/livecodes/i18n/locales/nl/translation.lokalise.json
+++ b/src/livecodes/i18n/locales/nl/translation.lokalise.json
@@ -1992,6 +1992,9 @@
"templates.starter.duckdb": {
"translation": "DuckDB-starter"
},
+ "templates.starter.elixir-wasm": {
+ "translation": "Elixir-starter"
+ },
"templates.starter.elm": {
"translation": "Elm-starter"
},
diff --git a/src/livecodes/i18n/locales/nl/translation.ts b/src/livecodes/i18n/locales/nl/translation.ts
index b74213f83..7a9414e12 100644
--- a/src/livecodes/i18n/locales/nl/translation.ts
+++ b/src/livecodes/i18n/locales/nl/translation.ts
@@ -1003,6 +1003,7 @@ const translation: I18nTranslation = {
daisyui: 'daisyUI-starter',
dart: 'Dart-starter',
diagrams: 'Diagrammen-starter',
+ 'elixir-wasm': 'Elixir-starter',
elm: 'Elm-starter',
fanak: 'Fanak-starter',
fennel: 'Fennel-starter',
diff --git a/src/livecodes/i18n/locales/pt/language-info.lokalise.json b/src/livecodes/i18n/locales/pt/language-info.lokalise.json
index 9e24a65d8..60f861b89 100644
--- a/src/livecodes/i18n/locales/pt/language-info.lokalise.json
+++ b/src/livecodes/i18n/locales/pt/language-info.lokalise.json
@@ -261,6 +261,15 @@
"ejs.name": {
"translation": "EJS"
},
+ "elixirWasm.desc": {
+ "translation": "Compilador Elixir a funcionar no WebAssembly, usando AtomVM"
+ },
+ "elixirWasm.link": {
+ "translation": " Site do Elixir Documentação da linguagem Elixir Aprenda X em Y minutos, em que X = Elixir Documentação do LiveCodes Carregar modelo inicial "
+ },
+ "elixirWasm.name": {
+ "translation": "Elixir (Wasm)"
+ },
"elm.desc": {
"translation": "O Elm é compilado para JavaScript no navegador por uma versão do compilador Elm para WebAssembly."
},
diff --git a/src/livecodes/i18n/locales/pt/language-info.ts b/src/livecodes/i18n/locales/pt/language-info.ts
index 0f6c71dee..43b2606a8 100644
--- a/src/livecodes/i18n/locales/pt/language-info.ts
+++ b/src/livecodes/i18n/locales/pt/language-info.ts
@@ -145,6 +145,11 @@ const languageInfo: I18nLangInfoTranslation = {
link: '<1><2>Site oficial2>1> <3> <4>Documentação do LiveCodes4> 3>',
name: 'EJS',
},
+ elixirWasm: {
+ desc: 'Compilador Elixir a funcionar no WebAssembly, usando <1>AtomVM1>',
+ link: '<1> <2>Site do Elixir2> 1> <3> <4>Documentação da linguagem Elixir4> 3> <5> <6>Aprenda X em Y minutos, em que X = Elixir6> 5> <7> <8>Documentação do LiveCodes8> 7> <9> <10>Carregar modelo inicial10> 9>',
+ name: 'Elixir (Wasm)',
+ },
elm: {
desc: 'O Elm é compilado para JavaScript no navegador por uma versão do compilador Elm para WebAssembly.',
link: '<1><2>Site oficial do Elm2>1> <3><4>Guia do Elm4>3> <5> <6>Documentação do LiveCodes6> 5> <7> <8>Carregar modelo inicial8> 7>',
diff --git a/src/livecodes/i18n/locales/pt/translation.lokalise.json b/src/livecodes/i18n/locales/pt/translation.lokalise.json
index ec59ffe9a..4fd70e09e 100644
--- a/src/livecodes/i18n/locales/pt/translation.lokalise.json
+++ b/src/livecodes/i18n/locales/pt/translation.lokalise.json
@@ -1992,6 +1992,9 @@
"templates.starter.duckdb": {
"translation": "Iniciante DuckDB"
},
+ "templates.starter.elixir-wasm": {
+ "translation": "Iniciante Elixir"
+ },
"templates.starter.elm": {
"translation": "Iniciante Elm"
},
diff --git a/src/livecodes/i18n/locales/pt/translation.ts b/src/livecodes/i18n/locales/pt/translation.ts
index 5b73c638f..51012577c 100644
--- a/src/livecodes/i18n/locales/pt/translation.ts
+++ b/src/livecodes/i18n/locales/pt/translation.ts
@@ -1003,6 +1003,7 @@ const translation: I18nTranslation = {
daisyui: 'Iniciante daisyUI',
dart: 'Iniciante Dart',
diagrams: 'Iniciante Diagrams',
+ 'elixir-wasm': 'Iniciante Elixir',
elm: 'Iniciante Elm',
fanak: 'Iniciante Fanak',
fennel: 'Iniciante Fennel',
diff --git a/src/livecodes/i18n/locales/ru/language-info.lokalise.json b/src/livecodes/i18n/locales/ru/language-info.lokalise.json
index dae9af844..94c2396c9 100644
--- a/src/livecodes/i18n/locales/ru/language-info.lokalise.json
+++ b/src/livecodes/i18n/locales/ru/language-info.lokalise.json
@@ -261,6 +261,15 @@
"ejs.name": {
"translation": "EJS"
},
+ "elixirWasm.desc": {
+ "translation": "Компилятор Elixir, работающий на WebAssembly, с использованием AtomVM"
+ },
+ "elixirWasm.link": {
+ "translation": " Сайт Elixir Документация по языку Elixir Изучите X за Y минут, где X = Elixir Документация LiveCodes Загрузить стартовый шаблон "
+ },
+ "elixirWasm.name": {
+ "translation": "Elixir (Wasm)"
+ },
"elm.desc": {
"translation": "Elm компилируется в JavaScript в браузере с помощью версии компилятора Elm, оптимизированной для WebAssembly."
},
diff --git a/src/livecodes/i18n/locales/ru/language-info.ts b/src/livecodes/i18n/locales/ru/language-info.ts
index c2dc74f98..9e2efc095 100644
--- a/src/livecodes/i18n/locales/ru/language-info.ts
+++ b/src/livecodes/i18n/locales/ru/language-info.ts
@@ -146,6 +146,11 @@ const languageInfo: I18nLangInfoTranslation = {
link: '<1><2>Официальный сайт2>1> <3> <4>Документация LiveCodes4> 3>',
name: 'EJS',
},
+ elixirWasm: {
+ desc: 'Компилятор Elixir, работающий на WebAssembly, с использованием <1>AtomVM1>',
+ link: '<1> <2>Сайт Elixir2> 1> <3> <4>Документация по языку Elixir4> 3> <5> <6>Изучите X за Y минут, где X = Elixir6> 5> <7> <8>Документация LiveCodes8> 7> <9> <10>Загрузить стартовый шаблон10> 9>',
+ name: 'Elixir (Wasm)',
+ },
elm: {
desc: 'Elm компилируется в JavaScript в браузере с помощью версии компилятора Elm, оптимизированной для WebAssembly.',
link: '<1><2>Официальный сайт Elm2>1> <3><4>Руководство по Elm4>3> <5> <6>Документация по LiveCodes6> 5> <7> <8>Загрузить стартовый шаблон8> 7>',
diff --git a/src/livecodes/i18n/locales/ru/translation.lokalise.json b/src/livecodes/i18n/locales/ru/translation.lokalise.json
index b2269e3cf..47357510d 100644
--- a/src/livecodes/i18n/locales/ru/translation.lokalise.json
+++ b/src/livecodes/i18n/locales/ru/translation.lokalise.json
@@ -1992,6 +1992,9 @@
"templates.starter.duckdb": {
"translation": "Стартер DuckDB"
},
+ "templates.starter.elixir-wasm": {
+ "translation": "Стартер Elixir"
+ },
"templates.starter.elm": {
"translation": "Стартер Elm"
},
diff --git a/src/livecodes/i18n/locales/ru/translation.ts b/src/livecodes/i18n/locales/ru/translation.ts
index b5db663b5..cdd0fb448 100644
--- a/src/livecodes/i18n/locales/ru/translation.ts
+++ b/src/livecodes/i18n/locales/ru/translation.ts
@@ -1002,6 +1002,7 @@ const translation: I18nTranslation = {
daisyui: 'Стартер daisyUI',
dart: 'Стартер Dart',
diagrams: 'Стартер диаграмм',
+ 'elixir-wasm': 'Стартер Elixir',
elm: 'Стартер Elm',
fanak: 'Стартер Fanak',
fennel: 'Стартер Fennel',
diff --git a/src/livecodes/i18n/locales/tr/language-info.lokalise.json b/src/livecodes/i18n/locales/tr/language-info.lokalise.json
index 79c4b1c82..ef300f65e 100644
--- a/src/livecodes/i18n/locales/tr/language-info.lokalise.json
+++ b/src/livecodes/i18n/locales/tr/language-info.lokalise.json
@@ -261,6 +261,15 @@
"ejs.name": {
"translation": "EJS"
},
+ "elixirWasm.desc": {
+ "translation": "WebAssembly üzerinde çalışan Elixir derleyicisi, AtomVM kullanılarak."
+ },
+ "elixirWasm.link": {
+ "translation": " Elixir web sitesi Elixir dil belgeleri Y dakikada X'i öğrenin, burada X=Elixir LiveCodes Belgeleri Başlangıç şablonunu yükle "
+ },
+ "elixirWasm.name": {
+ "translation": "Elixir (Wasm)"
+ },
"elm.desc": {
"translation": "Elm, Elm derleyicisinin bir WebAssembly derlemesi aracılığıyla tarayıcıda JavaScript'e derlenir."
},
diff --git a/src/livecodes/i18n/locales/tr/language-info.ts b/src/livecodes/i18n/locales/tr/language-info.ts
index 7f9d05116..36cfca970 100644
--- a/src/livecodes/i18n/locales/tr/language-info.ts
+++ b/src/livecodes/i18n/locales/tr/language-info.ts
@@ -145,6 +145,11 @@ const languageInfo: I18nLangInfoTranslation = {
link: '<1><2>Resmi web sitesi2>1> <3> <4>LiveCodes Dokümantasyonu4> 3>',
name: 'EJS',
},
+ elixirWasm: {
+ desc: 'WebAssembly üzerinde çalışan Elixir derleyicisi, <1>AtomVM1> kullanılarak.',
+ link: "<1> <2>Elixir web sitesi2> 1> <3> <4>Elixir dil belgeleri4> 3> <5> <6>Y dakikada X'i öğrenin, burada X=Elixir6> 5> <7> <8>LiveCodes Belgeleri8> 7> <9> <10>Başlangıç şablonunu yükle10> 9>",
+ name: 'Elixir (Wasm)',
+ },
elm: {
desc: "Elm, Elm derleyicisinin bir WebAssembly derlemesi aracılığıyla tarayıcıda JavaScript'e derlenir.",
link: '<1><2>Elm resmi web sitesi2>1> <3><4>Elm kılavuzu4>3> <5> <6>LiveCodes Belgeleri6> 5> <7> <8>Başlangıç şablonunu yükle8> 7>',
diff --git a/src/livecodes/i18n/locales/tr/translation.lokalise.json b/src/livecodes/i18n/locales/tr/translation.lokalise.json
index f7879cba0..aaa8ea4ac 100644
--- a/src/livecodes/i18n/locales/tr/translation.lokalise.json
+++ b/src/livecodes/i18n/locales/tr/translation.lokalise.json
@@ -1992,6 +1992,9 @@
"templates.starter.duckdb": {
"translation": "DuckDB Başlangıç"
},
+ "templates.starter.elixir-wasm": {
+ "translation": "Elixir Başlangıç"
+ },
"templates.starter.elm": {
"translation": "Elm Başlangıç"
},
diff --git a/src/livecodes/i18n/locales/tr/translation.ts b/src/livecodes/i18n/locales/tr/translation.ts
index 78d45c04b..a61513cf2 100644
--- a/src/livecodes/i18n/locales/tr/translation.ts
+++ b/src/livecodes/i18n/locales/tr/translation.ts
@@ -1003,6 +1003,7 @@ const translation: I18nTranslation = {
daisyui: 'daisyUI Başlangıç',
dart: 'Dart Başlangıç',
diagrams: 'Diyagramlar Başlangıç',
+ 'elixir-wasm': 'Elixir Başlangıç',
elm: 'Elm Başlangıç',
fanak: 'Fanak Başlangıç',
fennel: 'Fennel Başlangıç',
diff --git a/src/livecodes/i18n/locales/ur/language-info.lokalise.json b/src/livecodes/i18n/locales/ur/language-info.lokalise.json
index 03b8c931a..aa7b74259 100644
--- a/src/livecodes/i18n/locales/ur/language-info.lokalise.json
+++ b/src/livecodes/i18n/locales/ur/language-info.lokalise.json
@@ -261,6 +261,15 @@
"ejs.name": {
"translation": "ای جے ایس"
},
+ "elixirWasm.desc": {
+ "translation": "Elixir کمپائلر WebAssembly پر چل رہا ہے، استعمال کر رہا ہے AtomVM"
+ },
+ "elixirWasm.link": {
+ "translation": " Elixir ویب سائٹ Elixir زبان کی دستاویزات X=Elixir — Elixir کو چند منٹوں میں سیکھیں LiveCodes دستاویزات اسٹارٹر ٹیمپلیٹ لوڈ کریں "
+ },
+ "elixirWasm.name": {
+ "translation": "Elixir (Wasm)"
+ },
"elm.desc": {
"translation": "ایلم (Elm) کو براؤزر میں ایلم کمپائلر کے ویب اسمبلی (WebAssembly) بلڈ کے ذریعے جاوا اسکرپٹ میں کمپائل کیا جاتا ہے۔"
},
diff --git a/src/livecodes/i18n/locales/ur/language-info.ts b/src/livecodes/i18n/locales/ur/language-info.ts
index 5037e9f81..8638d07e1 100644
--- a/src/livecodes/i18n/locales/ur/language-info.ts
+++ b/src/livecodes/i18n/locales/ur/language-info.ts
@@ -146,6 +146,11 @@ const languageInfo: I18nLangInfoTranslation = {
link: '<1><2>سرکاری ویب سائٹ2>1> <3> <4>لائیو کوڈز دستاویزات4> 3>',
name: 'ای جے ایس',
},
+ elixirWasm: {
+ desc: 'Elixir کمپائلر WebAssembly پر چل رہا ہے، استعمال کر رہا ہے <1>AtomVM1>',
+ link: '<1> <2>Elixir ویب سائٹ2> 1> <3> <4>Elixir زبان کی دستاویزات4> 3> <5> <6>X=Elixir — Elixir کو چند منٹوں میں سیکھیں6> 5> <7> <8>LiveCodes دستاویزات8> 7> <9> <10>اسٹارٹر ٹیمپلیٹ لوڈ کریں10> 9>',
+ name: 'Elixir (Wasm)',
+ },
elm: {
desc: 'ایلم (Elm) کو براؤزر میں ایلم کمپائلر کے ویب اسمبلی (WebAssembly) بلڈ کے ذریعے جاوا اسکرپٹ میں کمپائل کیا جاتا ہے۔',
link: '<1><2>Elm کی سرکاری ویب سائٹ2>1> <3><4>Elm گائیڈ4>3> <5> <6>LiveCodes دستاویزی مواد6> 5> <7> <8>سٹارٹر ٹیمپلیٹ لوڈ کریں8> 7>',
diff --git a/src/livecodes/i18n/locales/ur/translation.lokalise.json b/src/livecodes/i18n/locales/ur/translation.lokalise.json
index c211847c4..2830f0328 100644
--- a/src/livecodes/i18n/locales/ur/translation.lokalise.json
+++ b/src/livecodes/i18n/locales/ur/translation.lokalise.json
@@ -1992,6 +1992,9 @@
"templates.starter.duckdb": {
"translation": "ڈک ڈی بی شروعاتی"
},
+ "templates.starter.elixir-wasm": {
+ "translation": "Elixir شروعاتی"
+ },
"templates.starter.elm": {
"translation": "ایلم شروعاتی"
},
diff --git a/src/livecodes/i18n/locales/ur/translation.ts b/src/livecodes/i18n/locales/ur/translation.ts
index c100013f9..c6471075c 100644
--- a/src/livecodes/i18n/locales/ur/translation.ts
+++ b/src/livecodes/i18n/locales/ur/translation.ts
@@ -1002,6 +1002,7 @@ const translation: I18nTranslation = {
daisyui: 'daisyUI شروعاتی',
dart: 'Dart شروعاتی',
diagrams: 'ڈایاگرامز شروعاتی',
+ 'elixir-wasm': 'Elixir شروعاتی',
elm: 'ایلم شروعاتی',
fanak: 'Fanak شروعاتی',
fennel: 'فینل شروعاتی',
diff --git a/src/livecodes/i18n/locales/zh-CN/language-info.lokalise.json b/src/livecodes/i18n/locales/zh-CN/language-info.lokalise.json
index ffb1e76ff..b30c4134f 100644
--- a/src/livecodes/i18n/locales/zh-CN/language-info.lokalise.json
+++ b/src/livecodes/i18n/locales/zh-CN/language-info.lokalise.json
@@ -261,6 +261,15 @@
"ejs.name": {
"translation": "EJS"
},
+ "elixirWasm.desc": {
+ "translation": "在 WebAssembly 上运行的 Elixir 编译器,使用AtomVM"
+ },
+ "elixirWasm.link": {
+ "translation": " Elixir 网站 Elixir 语言文档 Y分钟掌握X,其中X=Elixir LiveCodes 文档 加载入门模板 "
+ },
+ "elixirWasm.name": {
+ "translation": "Elixir (Wasm)"
+ },
"elm.desc": {
"translation": "Elm 通过 Elm 编译器的 WebAssembly 构建版本在浏览器中编译为 JavaScript。"
},
diff --git a/src/livecodes/i18n/locales/zh-CN/language-info.ts b/src/livecodes/i18n/locales/zh-CN/language-info.ts
index 6b88d8b46..ea244df6b 100644
--- a/src/livecodes/i18n/locales/zh-CN/language-info.ts
+++ b/src/livecodes/i18n/locales/zh-CN/language-info.ts
@@ -144,6 +144,11 @@ const languageInfo: I18nLangInfoTranslation = {
link: '<1><2>官方网站2>1> <3> <4>LiveCodes 文档4> 3>',
name: 'EJS',
},
+ elixirWasm: {
+ desc: '在 WebAssembly 上运行的 Elixir 编译器,使用<1>AtomVM1>',
+ link: '<1> <2>Elixir 网站2> 1> <3> <4>Elixir 语言文档4> 3> <5> <6>Y分钟掌握X,其中X=Elixir6> 5> <7> <8>LiveCodes 文档8> 7> <9> <10>加载入门模板10> 9>',
+ name: 'Elixir (Wasm)',
+ },
elm: {
desc: 'Elm 通过 Elm 编译器的 WebAssembly 构建版本在浏览器中编译为 JavaScript。',
link: '<1><2>Elm 官方网站2>1> <3><4>Elm 指南4>3> <5> <6>LiveCodes 文档6> 5> <7> <8>加载入门模板8> 7>',
diff --git a/src/livecodes/i18n/locales/zh-CN/translation.lokalise.json b/src/livecodes/i18n/locales/zh-CN/translation.lokalise.json
index 7b8c965e1..c30ee36c1 100644
--- a/src/livecodes/i18n/locales/zh-CN/translation.lokalise.json
+++ b/src/livecodes/i18n/locales/zh-CN/translation.lokalise.json
@@ -1992,6 +1992,9 @@
"templates.starter.duckdb": {
"translation": "DuckDB 启动模板"
},
+ "templates.starter.elixir-wasm": {
+ "translation": "Elixir 启动模板"
+ },
"templates.starter.elm": {
"translation": "Elm 启动模板"
},
diff --git a/src/livecodes/i18n/locales/zh-CN/translation.ts b/src/livecodes/i18n/locales/zh-CN/translation.ts
index d63f874ed..ce80e6d9c 100644
--- a/src/livecodes/i18n/locales/zh-CN/translation.ts
+++ b/src/livecodes/i18n/locales/zh-CN/translation.ts
@@ -1000,6 +1000,7 @@ const translation: I18nTranslation = {
daisyui: 'daisyUI 启动模板',
dart: 'Dart 启动模板',
diagrams: '图表启动模板',
+ 'elixir-wasm': 'Elixir 启动模板',
elm: 'Elm 启动模板',
fanak: 'Fanak 启动模板',
fennel: 'Fennel 启动模板',
diff --git a/src/livecodes/languages/beam-wasm/lang-beam-wasm-worker.raw.js b/src/livecodes/languages/beam-wasm/lang-beam-wasm-worker.raw.js
new file mode 100644
index 000000000..609594d3e
--- /dev/null
+++ b/src/livecodes/languages/beam-wasm/lang-beam-wasm-worker.raw.js
@@ -0,0 +1,299 @@
+/* eslint-disable no-eval -- indirect eval is required by AtomVM JS interop (onRunTrackedJs) */
+// @ts-nocheck -- vendored fixed Popcorn worker runtime (plain JS, typed as text import)
+/*
+ * Persistent Popcorn worker runtime for LiveCodes Beam (Elixir/Erlang) Wasm.
+ *
+ * Based on Software Mansion Popcorn's iframe runtime, originally vendored through
+ * `live-codes/elixir-browser-eval`.
+ * The runtime is hosted in a Worker owned by the result window, not in an
+ * iframe owned by the result document. `document.open()/write()/close()` used
+ * by LiveReload replaces the document but keeps the window (and therefore this
+ * worker) alive, so a code edit no longer reboots the VM.
+ *
+ * Changes from the upstream iframe runtime:
+ *
+ * 1. `BUNDLE_PATH` is injected by the main thread when the Blob worker is
+ * created, so the runtime does not depend on a DOM meta element.
+ * 2. Messages go through `self` instead of `window.parent`. The main thread
+ * owns the Worker object, so no cross-origin relay is required.
+ * 3. The AtomVM build is single-threaded (no Emscripten pthreads), so the
+ * runtime runs entirely in this Worker. `main()` is started explicitly with
+ * `Module.callMain()` after the JS interop hooks are installed; otherwise the
+ * synchronous Elixir boot would call `onElixirReady` before the hooks exist.
+ * 4. The Elixir boot code generated by the pinned bundle expects
+ * `window.parent.document` in its JS interop. A minimal shim is installed
+ * immediately before `init()`; it is deliberately installed after the
+ * AtomVM module is imported so Emscripten still detects a worker
+ * environment.
+ *
+ * Message protocol (postMessage types) is unchanged from upstream, with an
+ * extra `popcorn-timing` diagnostic channel used by the benchmarks/tests.
+ */
+
+const MESSAGES = {
+ INIT: 'popcorn-init',
+ START_VM: 'popcorn-startVm',
+ CALL: 'popcorn-call',
+ CAST: 'popcorn-cast',
+ CALL_ACK: 'popcorn-callAck',
+ STDOUT: 'popcorn-stdout',
+ STDERR: 'popcorn-stderr',
+ HEARTBEAT: 'popcorn-heartbeat',
+ RELOAD: 'popcorn-reload',
+ TIMING: 'popcorn-timing',
+ FATAL: 'popcorn-fatal',
+};
+
+const BUNDLE_PATH = '__LIVECODES_BEAM_BUNDLE_URL__';
+
+const HEARTBEAT_INTERVAL_MS = 500;
+
+let Module = null;
+let binaryDir = '';
+
+const mark = (name, detail) => {
+ send(MESSAGES.TIMING, { name, at: performance.now(), detail });
+};
+mark('worker-start');
+
+// The binaries sit next to the bundle, which may be a CDN mirror.
+const locateBinary = (path) => binaryDir + path;
+
+class TrackedValue {
+ // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility -- plain JS file, no TS modifiers
+ constructor({ key, value }) {
+ if (typeof key !== 'number') {
+ throw new Error('key property in TrackedValue must be a number');
+ }
+ this.key = key;
+ this.value = value;
+ }
+}
+
+globalThis.TrackedValue = TrackedValue;
+
+async function runWorker() {
+ try {
+ await startPopcorn();
+ } catch (error) {
+ // A worker that dies silently is near-impossible to debug — report it and
+ // let the main thread reject the boot waiters immediately instead of
+ // waiting for the init timeout.
+ const detail = (error && (error.stack || error.message)) || String(error);
+ send(MESSAGES.STDERR, `Runtime failed: ${detail}\n`);
+ send(MESSAGES.FATAL, { message: detail });
+ throw error;
+ }
+}
+
+async function startPopcorn() {
+ const bundlePath = BUNDLE_PATH;
+ binaryDir = new URL('.', bundlePath).href;
+
+ mark('atomvm-import-start');
+ const { default: init } = await import(binaryDir + 'AtomVM.mjs');
+ mark('atomvm-import-end');
+
+ mark('bundle-fetch-start');
+ const bundleBuffer = await fetch(bundlePath).then((resp) => resp.arrayBuffer());
+ const bundle = new Int8Array(bundleBuffer);
+ mark('bundle-fetch-end', { bytes: bundle.length });
+ send(MESSAGES.INIT);
+ // init() hanging would leave the playground loading forever with no output,
+ // so fail loudly instead: the parent surfaces everything sent before dying.
+ const startTimeout = new Promise((resolve, reject) => {
+ setTimeout(() => reject(new Error('AtomVM start timed out (VM did not boot).')), 100000);
+ });
+ mark('vm-init-start');
+ const initProcess = await Promise.race([startVm(bundle, init), startTimeout]);
+ mark('vm-init-end');
+
+ self.addEventListener('message', async ({ data }) => {
+ const type = data.type;
+
+ if (type === MESSAGES.CALL) {
+ await handleCall(data);
+ } else if (type.startsWith('popcorn')) {
+ send(MESSAGES.STDERR, `Worker: received unhandled popcorn event: ${JSON.stringify(data)}\n`);
+ }
+ });
+
+ send(MESSAGES.START_VM, initProcess);
+ mark('vm-ready');
+ setInterval(() => send(MESSAGES.HEARTBEAT, null), HEARTBEAT_INTERVAL_MS);
+}
+
+async function startVm(avmBundle, init) {
+ let resolveResultPromise = null;
+ const resultPromise = new Promise((resolve) => {
+ resolveResultPromise = resolve;
+ });
+ // The pinned Elixir boot bundle was built for an iframe and its JS interop
+ // reads `globalThis.window.parent.document` before calling back into
+ // `Module.onElixirReady`. Install the smallest possible shim after the
+ // AtomVM module has been imported (so Emscripten's worker detection is not
+ // affected) and before init() starts the Elixir boot process.
+ globalThis.window = globalThis;
+ globalThis.window.parent = globalThis;
+ globalThis.window.document = globalThis.document || {};
+ Module = await init({
+ noInitialRun: true,
+ locateFile: locateBinary,
+ preRun: [
+ function ({ FS }) {
+ FS.mkdir('/data');
+ FS.writeFile('/data/bundle.avm', avmBundle);
+ },
+ ],
+ print(text) {
+ send(MESSAGES.STDOUT, text);
+ },
+ printErr(text) {
+ send(MESSAGES.STDERR, text);
+ },
+ onAbort() {
+ // Timeout so that error logs are (hopefully) printed
+ // before we terminate
+ setTimeout(() => send(MESSAGES.RELOAD, null), 100);
+ },
+ });
+
+ Module.serialize = JSON.stringify;
+ Module.deserialize = deserialize;
+ Module.cleanupFunctions = new Map();
+ Module.onTrackedObjectDelete = (key) => {
+ const fns = Module.cleanupFunctions;
+ const fn = fns.get(key);
+ fns.delete(key);
+ try {
+ if (fn) fn();
+ } catch (e) {
+ send(
+ MESSAGES.STDERR,
+ `onTrackedObjectDelete failed: ${(e && (e.stack || e.message)) || e}\n`,
+ );
+ } finally {
+ Module.trackedObjectsMap.delete(key);
+ }
+ };
+ const origCast = Module.cast;
+ const origCall = Module.call;
+
+ Module.cast = (process, args) => {
+ const serialized = Module.serialize(args);
+ origCast(process, serialized);
+ };
+ Module.call = (process, args) => {
+ const serialized = Module.serialize(args);
+ return origCall(process, serialized);
+ };
+
+ Module.onRunTrackedJs = (scriptString, isDebug) => {
+ const trackValue = (tracked) => {
+ const getKey = Module.nextTrackedObjectKey;
+ const map = Module.trackedObjectsMap;
+
+ if (tracked instanceof TrackedValue) {
+ map.set(tracked.key, tracked.value);
+ return tracked.key;
+ }
+
+ const key = getKey();
+ map.set(key, tracked);
+ return key;
+ };
+
+ let fn;
+ try {
+ const indirectEval = eval;
+ fn = indirectEval(scriptString); // NOSONAR - required by AtomVM's sandboxed JS interop
+ } catch (e) {
+ send(MESSAGES.STDERR, `onRunTrackedJs eval failed: ${(e && (e.stack || e.message)) || e}\n`);
+ return null;
+ }
+ if (isDebug) ensureFunctionEval(fn);
+ let result;
+ try {
+ result = fn(Module);
+ } catch (e) {
+ send(MESSAGES.STDERR, `onRunTrackedJs failed: ${(e && (e.stack || e.message)) || e}\n`);
+ return null;
+ }
+ if (isDebug) ensureResultKeyList(result);
+
+ return result == null ? [] : result.map(trackValue);
+ };
+ Module.onGetTrackedObjects = (keys) => {
+ const getTrackedObject = (key) => {
+ const serialize = Module.serialize;
+ const map = Module.trackedObjectsMap;
+
+ return serialize(map.get(key));
+ };
+ return keys.map(getTrackedObject);
+ };
+
+ Module.onElixirReady = (initProcess) => {
+ Module.onElixirReady = null;
+ resolveResultPromise(initProcess);
+ };
+
+ Module.callMain(['/data/bundle.avm']);
+
+ return resultPromise;
+}
+
+async function handleCall(data) {
+ const { requestId, process, args } = data.value;
+ send(MESSAGES.CALL_ACK, { requestId });
+ mark('call-start', { requestId });
+
+ try {
+ const result = await Module.call(process, args);
+ mark('call-end', { requestId });
+ send(MESSAGES.CALL, { requestId, data: Module.deserialize(result) });
+ } catch (error) {
+ mark('call-error', { requestId, error: String(error) });
+ if (error === 'noproc') {
+ send(MESSAGES.RELOAD, null);
+ send(MESSAGES.STDERR, 'Runtime VM crashed, worker will be restarted.\n');
+ return;
+ }
+ send(MESSAGES.CALL, { requestId, error: Module.deserialize(error) });
+ }
+}
+
+function ensureFunctionEval(maybeFunction) {
+ if (typeof maybeFunction !== 'function') {
+ throw new Error('Script passed to onRunTrackedJs() is not wrapped in a function');
+ }
+}
+
+function ensureResultKeyList(result) {
+ if (!Array.isArray(result) && result !== undefined) {
+ throw new Error(
+ 'Script passed to onRunTrackedJs() returned invalid value, accepted values are arrays and undefined',
+ );
+ }
+}
+
+function send(type, data) {
+ self.postMessage({ type, value: data });
+}
+
+function deserialize(message) {
+ return JSON.parse(message, (key, value) => {
+ const isRef =
+ typeof value === 'object' &&
+ value !== null &&
+ Object.hasOwn(value, 'popcorn_ref') &&
+ Object.getOwnPropertyNames(value).length === 1;
+
+ if (!isRef) {
+ return value;
+ }
+ return Module.trackedObjectsMap.get(value.popcorn_ref);
+ });
+}
+
+void runWorker();
diff --git a/src/livecodes/languages/beam-wasm/prelude.ts b/src/livecodes/languages/beam-wasm/prelude.ts
new file mode 100644
index 000000000..231ac2ad8
--- /dev/null
+++ b/src/livecodes/languages/beam-wasm/prelude.ts
@@ -0,0 +1,41 @@
+// AtomVM compiles this module on first boot, and compile time is the dominant
+// cold-start cost. Keep it small; the worker keeps the compiled module alive
+// across LiveReload edits. The device is shared by Elixir and Erlang.
+export const BEAM_PRELUDE = `defmodule :livecodes_io do
+ def start(stdin) do
+ lines =
+ stdin
+ |> to_string()
+ |> String.split("\\n", trim: true)
+ |> Enum.map(&(&1 <> "\\n"))
+
+ spawn(fn -> loop(lines) end)
+ end
+
+ def stop(dev), do: send(dev, :lc_stop)
+
+ def halt, do: throw({:lc_exit, 0})
+ def halt(status), do: throw({:lc_exit, status})
+
+ defp loop(lines) do
+ receive do
+ :lc_stop -> :ok
+ {:io_request, from, reply_as, req} ->
+ {reply, rest} = handle(req, lines)
+ send(from, {:io_reply, reply_as, reply})
+ loop(rest)
+ end
+ end
+
+ defp handle({:get_line, _enc, _prompt}, [line | rest]), do: {line, rest}
+ defp handle({:get_line, _enc, _prompt}, []), do: {:eof, []}
+ defp handle({:put_chars, _enc, chars}, lines) do
+ IO.write(chars)
+ {:ok, lines}
+ end
+ defp handle({:put_chars, chars}, lines) do
+ IO.write(chars)
+ {:ok, lines}
+ end
+ defp handle(other, lines), do: {{:error, {:enotsup, other}}, lines}
+end`;
diff --git a/src/livecodes/languages/beam-wasm/runtime.ts b/src/livecodes/languages/beam-wasm/runtime.ts
new file mode 100644
index 000000000..00d6c54b1
--- /dev/null
+++ b/src/livecodes/languages/beam-wasm/runtime.ts
@@ -0,0 +1,856 @@
+/* eslint-disable no-console */
+import { getErrorMessage } from '../../utils/utils';
+import { createLoadingReporter } from '../wasm-runtime';
+// @ts-ignore
+// eslint-disable-next-line import/no-unresolved
+import workerSource from './lang-beam-wasm-worker.raw.js?raw';
+
+export interface RunResult {
+ output: string | null;
+ error: string | null;
+ exitCode: number | null;
+ value?: string | null;
+ warnings?: string | null;
+ cancelled?: boolean;
+}
+
+export interface ParsedResult {
+ value: string | null;
+ error: string | null;
+ exitCode: number;
+}
+
+export interface BeamTiming {
+ name: string;
+ at: number;
+ detail?: Record;
+}
+
+export interface BeamApi {
+ ready: boolean;
+ failed?: boolean;
+ input?: string;
+ output: string | null;
+ error: string | null;
+ exitCode: number | null;
+ value?: string | null;
+ warnings?: string | null;
+ loaded: Promise;
+ evals?: number;
+ runId?: number;
+ currentRun?: Promise | null;
+ init?: Promise;
+ ensureInit?: () => Promise;
+ retry?: () => Promise;
+ popcorn?: any;
+ stdout?: string[];
+ stderr?: string[];
+ timings?: BeamTiming[];
+ run: (input?: string) => Promise;
+}
+
+export interface BeamRuntimeConfig {
+ /** Key on `window.livecodes` used by the public API, e.g. `elixir`. */
+ apiKey: string;
+ /** Optional alias key, e.g. `elixir-wasm`. */
+ apiAlias?: string;
+ /** Human-readable language name used in error messages. */
+ label: string;
+ /** Result script tag type, e.g. `text/elixir-wasm`. */
+ scriptType: string;
+ /** Pinned runtime bundle URL. */
+ bundleUrl: string;
+ /** Language prelude, compiled once per worker with `eval_elixir`. */
+ prelude: string;
+ /** Builds the runtime call payload for a run. */
+ buildCall: (code: string, stdin: string) => unknown[];
+ /** Parses the language-specific result protocol. */
+ parseResult: (text: string) => ParsedResult;
+ /** Parses the Elixir prelude result. Defaults to `parseTaggedResult`. */
+ parsePreludeResult?: (text: string) => ParsedResult;
+ /** Normalizes the raw runtime result before parsing. */
+ normalizeResult?: (data: unknown) => string;
+ /** Whether the wrapping language adds a trailing newline that should be dropped. */
+ stripTrailingNewline?: boolean;
+}
+
+declare const window: Window & {
+ livecodes: Record;
+};
+
+const EVAL_TIMEOUT_MS = 60_000;
+// First boot downloads ~8MB (bundle.avm + AtomVM.wasm) and starts the VM,
+// so allow generous time before giving up.
+const INIT_VM_TIMEOUT_MS = 120_000;
+const SUPERSEDE_TIMEOUT_MS = 10_000;
+
+// Message protocol spoken by the runtime worker (same types as upstream Popcorn).
+const bridgeMessages = {
+ init: 'popcorn-init',
+ startVm: 'popcorn-startVm',
+ call: 'popcorn-call',
+ callAck: 'popcorn-callAck',
+ stdout: 'popcorn-stdout',
+ stderr: 'popcorn-stderr',
+ heartbeat: 'popcorn-heartbeat',
+ reload: 'popcorn-reload',
+ timing: 'popcorn-timing',
+ fatal: 'popcorn-fatal',
+} as const;
+
+export const OK_TAG = '__LC_OK__';
+export const ERROR_TAG = '__LC_ERROR__';
+export const EXIT_TAG = '__LC_EXIT__';
+const LEGACY_EXIT_TAG = /^\{:?lc_exit, (-?\d+)\}$/;
+const LEGACY_ERROR_TAG = /^\{:?lc_error, ([\s\S]*)\}$/;
+
+export const decodeBase64 = (value: string): string => {
+ try {
+ const binary = atob(value);
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
+ return new TextDecoder().decode(bytes);
+ } catch {
+ return value;
+ }
+};
+
+// Popcorn serializes a returned Elixir string as a JSON string. A tagged
+// protocol string therefore arrives as `"\"__LC_OK__...\""`; unwrap it before
+// parsing. Atoms/tuples arrive without the extra quoting.
+export const normalizeResultText = (data: unknown): string => {
+ if (data == null) return '';
+ let text = String(data);
+ if (text.length >= 2 && text.startsWith('"') && text.endsWith('"')) {
+ try {
+ const parsed = JSON.parse(text);
+ if (typeof parsed === 'string') text = parsed;
+ } catch {
+ // Keep the original text if it is not valid JSON.
+ }
+ }
+ return text;
+};
+
+export const parseTaggedResult = (text: string, defaultError = 'Runtime error'): ParsedResult => {
+ if (text.startsWith(OK_TAG)) {
+ return { value: decodeBase64(text.slice(OK_TAG.length)), error: null, exitCode: 0 };
+ }
+ if (text.startsWith(EXIT_TAG)) {
+ const exitCode = Number(text.slice(EXIT_TAG.length));
+ return { value: null, error: null, exitCode: Number.isFinite(exitCode) ? exitCode : 1 };
+ }
+ if (text.startsWith(ERROR_TAG)) {
+ const [kind, message, stack] = text
+ .slice(ERROR_TAG.length)
+ .split('|')
+ .map((part) => decodeBase64(part));
+ const detail = message || stack || defaultError;
+ const prefix = kind && kind !== 'error' && kind !== 'syntax' ? `${kind}: ` : '';
+ return { value: null, error: `${prefix}${detail}`, exitCode: 1 };
+ }
+ const legacyExit = LEGACY_EXIT_TAG.exec(text);
+ if (legacyExit) {
+ return { value: null, error: null, exitCode: Number(legacyExit[1]) };
+ }
+ const legacyError = LEGACY_ERROR_TAG.exec(text);
+ if (legacyError) {
+ return { value: null, error: legacyError[1], exitCode: 1 };
+ }
+ return { value: text === '' ? null : text, error: null, exitCode: 0 };
+};
+
+export const readBeamCode = (scriptType: string): string => {
+ let code = '';
+ document.querySelectorAll(`script[type="${scriptType}"]`).forEach((script) => {
+ code += `${script.textContent ?? script.innerHTML}\n`;
+ });
+ return code;
+};
+
+const drain = (arr: string[]) => {
+ const out = arr.join('\n');
+ arr.length = 0;
+ return out;
+};
+
+interface PendingCall {
+ startTimeMs: number;
+ resolve: (data: any) => void;
+ reject: (reason?: unknown) => void;
+ timer: ReturnType | null;
+}
+
+interface Waiter {
+ resolve: (value: any) => void;
+ reject: (reason?: unknown) => void;
+ timer: ReturnType | null;
+}
+
+export const createBeamRuntime = (config: BeamRuntimeConfig): BeamApi => {
+ const postLoading = createLoadingReporter();
+ const normalizeResult = config.normalizeResult ?? normalizeResultText;
+ const stripTrailingNewline = config.stripTrailingNewline ?? true;
+
+ window.livecodes ??= {};
+ const existing = window.livecodes[config.apiKey] as BeamApi | undefined;
+ const timings: BeamTiming[] = existing?.timings ?? [];
+ const mark = (name: string, detail?: Record) => {
+ timings.push({ name, at: performance.now(), ...(detail == null ? {} : { detail }) });
+ if (timings.length > 1000) timings.splice(0, timings.length - 1000);
+ };
+
+ const api = (window.livecodes[config.apiKey] ??= {} as BeamApi);
+ api.ready = false;
+ api.evals = (api.evals ?? 0) + 1;
+ const thisEval = api.evals;
+ mark('script-eval', {
+ evalId: thisEval,
+ isolated: window.crossOriginIsolated,
+ sharedArrayBuffer: typeof SharedArrayBuffer !== 'undefined',
+ });
+
+ const readCode = () => readBeamCode(config.scriptType);
+
+ /**
+ * Main-thread side of the Beam runtime.
+ *
+ * The runtime runs in a Worker owned by the result window. LiveReload rewrites
+ * the result document with `document.open()/write()/close()`, which keeps the
+ * window (and therefore this Worker) alive; editing code no longer destroys
+ * the VM. The bridge is stored on `window.livecodes[apiKey]`, which also
+ * survives document rewrites.
+ */
+ class BeamBridge {
+ private bundlePath: string;
+ private label: string;
+ private markTiming: (name: string, detail?: Record) => void;
+ private onStdout: (text: string) => void;
+ private onStderr: (text: string) => void;
+ private onReady: ((bridge: BeamBridge) => Promise) | null;
+ private worker: Worker | null = null;
+ private workerUrl: string | null = null;
+ private initProcess: string | null = null;
+ private requestId = 0;
+ private calls = new Map();
+ private waiters = new Map();
+ private booting: Promise | null = null;
+ private ready = false;
+ private failed: string | null = null;
+ private generation = 0;
+ private destroyed = false;
+
+ private constructor(options: {
+ bundlePath: string;
+ label: string;
+ mark: (name: string, detail?: Record) => void;
+ onStdout?: (text: string) => void;
+ onStderr?: (text: string) => void;
+ onReady?: (bridge: BeamBridge) => Promise;
+ }) {
+ this.bundlePath = options.bundlePath;
+ this.label = options.label;
+ this.markTiming = options.mark;
+ this.onStdout = options.onStdout ?? console.log;
+ this.onStderr = options.onStderr ?? console.warn;
+ this.onReady = options.onReady ?? null;
+ }
+
+ public get error(): string | null {
+ return this.failed;
+ }
+
+ public get isReady(): boolean {
+ return this.ready && this.worker != null;
+ }
+
+ public static async init(options: {
+ bundlePath: string;
+ label: string;
+ mark: (name: string, detail?: Record) => void;
+ onStdout?: (text: string) => void;
+ onStderr?: (text: string) => void;
+ onReady?: (bridge: BeamBridge) => Promise;
+ }): Promise {
+ const bridge = new BeamBridge(options);
+ await bridge.start();
+ return bridge;
+ }
+
+ public ensureReady(): Promise {
+ return this.start();
+ }
+
+ public async call(args: unknown, options: { timeoutMs?: number } = {}): Promise {
+ await this.start();
+ if (!this.isReady || this.initProcess == null) {
+ throw new Error(this.failed || `${this.label} runtime is not ready`);
+ }
+ const requestId = this.requestId;
+ this.requestId += 1;
+ const targetProcess = this.initProcess;
+ const timeoutMs = options.timeoutMs ?? EVAL_TIMEOUT_MS;
+ const callPromise = new Promise((resolve, reject) => {
+ const timer = setTimeout(() => {
+ this.calls.delete(requestId);
+ // Module.call is synchronous inside the worker. If it does not return,
+ // terminating the worker is the only reliable way to unblock newer
+ // work. This happens on timeouts only, never on a normal edit.
+ this.stopWorker(`${this.label} evaluation timed out after ${timeoutMs} ms`);
+ reject(new Error(`${this.label} evaluation timed out after ${timeoutMs} ms`));
+ }, timeoutMs);
+ this.calls.set(requestId, { startTimeMs: performance.now(), resolve, reject, timer });
+ });
+ try {
+ this.post(bridgeMessages.call, { requestId, process: targetProcess, args });
+ } catch (err) {
+ const call = this.calls.get(requestId);
+ if (call != null) {
+ this.calls.delete(requestId);
+ if (call.timer != null) clearTimeout(call.timer);
+ call.reject(err);
+ }
+ }
+ return callPromise;
+ }
+
+ /** Terminate the worker without waiting for the current evaluation. */
+ public cancel(reason = 'superseded by a newer run'): void {
+ if (this.worker == null) return;
+ this.stopWorker(reason);
+ }
+
+ public async restart(): Promise {
+ this.stopWorker('manual restart');
+ await this.start();
+ }
+
+ public destroy(): void {
+ this.destroyed = true;
+ this.stopWorker('destroyed');
+ }
+
+ private async start(): Promise {
+ if (this.destroyed) throw new Error(`${this.label} runtime was destroyed`);
+ if (this.isReady) return;
+ if (this.booting != null) return this.booting;
+ const attempt = this.boot();
+ this.booting = attempt;
+ try {
+ await attempt;
+ } finally {
+ if (this.booting === attempt) this.booting = null;
+ }
+ }
+
+ private async boot(): Promise {
+ this.stopWorker('restarting');
+ this.failed = null;
+ const generation = this.generation;
+ this.markTiming('worker-create-start');
+ const source = String(workerSource).replace('__LIVECODES_BEAM_BUNDLE_URL__', this.bundlePath);
+ const url = URL.createObjectURL(new Blob([source], { type: 'text/javascript' }));
+ this.workerUrl = url;
+ try {
+ const worker = new Worker(url, { type: 'module' });
+ this.worker = worker;
+ worker.addEventListener('message', this.onWorkerMessage);
+ worker.addEventListener('error', this.onWorkerError);
+ worker.addEventListener('messageerror', this.onWorkerMessageError);
+ this.markTiming('worker-created');
+ const initPromise = this.waitFor(bridgeMessages.init, INIT_VM_TIMEOUT_MS);
+ const startPromise = this.waitFor(bridgeMessages.startVm, INIT_VM_TIMEOUT_MS);
+ await initPromise;
+ this.markTiming('worker-init-message');
+ this.initProcess = await startPromise;
+ if (generation !== this.generation) {
+ throw new Error(`${this.label} runtime was restarted`);
+ }
+ this.markTiming('worker-vm-ready');
+ this.ready = true;
+ if (this.onReady != null) {
+ await this.onReady(this);
+ }
+ this.markTiming('worker-prelude-ready');
+ } catch (err) {
+ this.stopWorker(`boot failed: ${getErrorMessage(err)}`);
+ throw err;
+ }
+ }
+
+ private waitFor(type: string, timeoutMs: number): Promise {
+ return new Promise((resolve, reject) => {
+ const waiter: Waiter = { resolve, reject, timer: null };
+ waiter.timer = setTimeout(() => {
+ const list = this.waiters.get(type);
+ const index = list?.indexOf(waiter) ?? -1;
+ if (list != null && index >= 0) list.splice(index, 1);
+ reject(new Error(`Timed out waiting for ${type}`));
+ }, timeoutMs);
+ const list = this.waiters.get(type) ?? [];
+ list.push(waiter);
+ this.waiters.set(type, list);
+ });
+ }
+
+ private resolveWaiter(type: string, value: any): void {
+ const list = this.waiters.get(type);
+ const waiter = list?.shift();
+ if (list != null && list.length === 0) this.waiters.delete(type);
+ if (waiter == null) return;
+ if (waiter.timer != null) clearTimeout(waiter.timer);
+ waiter.resolve(value);
+ }
+
+ private post(type: string, value: unknown): void {
+ if (this.worker == null) throw new Error(`${this.label} worker is not running`);
+ this.worker.postMessage({ type, value });
+ }
+
+ private onWorkerMessage = (event: MessageEvent): void => {
+ const data = event.data as { type?: unknown; value?: any } | null;
+ if (data == null || typeof data.type !== 'string') return;
+ const value = data.value;
+ switch (data.type) {
+ case bridgeMessages.init:
+ case bridgeMessages.startVm:
+ this.resolveWaiter(data.type, value);
+ break;
+ case bridgeMessages.stdout:
+ this.onStdout(typeof value === 'string' ? value : String(value));
+ break;
+ case bridgeMessages.stderr:
+ this.onStderr(typeof value === 'string' ? value : String(value));
+ break;
+ case bridgeMessages.call:
+ this.handleCall(value);
+ break;
+ case bridgeMessages.callAck:
+ break;
+ case bridgeMessages.heartbeat:
+ break;
+ case bridgeMessages.reload:
+ this.stopWorker(`${this.label} runtime requested a restart`);
+ break;
+ case bridgeMessages.fatal:
+ {
+ const detail =
+ typeof value?.message === 'string' ? value.message : 'unknown runtime failure';
+ this.onStderr(`Runtime failed: ${detail}\n`);
+ this.stopWorker(`${this.label} runtime failed: ${detail}`);
+ }
+ break;
+ case bridgeMessages.timing:
+ this.recordWorkerTiming(value);
+ break;
+ default:
+ break;
+ }
+ };
+
+ private onWorkerError = (event: ErrorEvent): void => {
+ const detail =
+ event.error instanceof Error && event.error.message
+ ? event.error.message
+ : event.message || 'unknown error';
+ const message = `${this.label} worker error: ${detail}`;
+ this.onStderr(`${message}\n`);
+ this.stopWorker(message);
+ };
+
+ private onWorkerMessageError = (): void => {
+ const message = `${this.label} worker message could not be deserialized`;
+ this.onStderr(`${message}\n`);
+ this.stopWorker(message);
+ };
+
+ private handleCall(value: { requestId: number; error?: unknown; data?: unknown }): void {
+ const call = this.calls.get(value.requestId);
+ if (call == null) return;
+ this.calls.delete(value.requestId);
+ if (call.timer != null) clearTimeout(call.timer);
+ if (value.error !== undefined) {
+ call.reject(value.error);
+ } else {
+ call.resolve({ data: value.data });
+ }
+ }
+
+ private recordWorkerTiming(value: {
+ name?: unknown;
+ at?: unknown;
+ detail?: Record;
+ }): void {
+ if (typeof value?.name !== 'string') return;
+ this.markTiming(`worker:${value.name}`, {
+ workerAt: typeof value.at === 'number' ? value.at : null,
+ ...(value.detail ?? {}),
+ });
+ }
+
+ private stopWorker(reason: string): void {
+ const pendingCalls = this.calls.size;
+ this.generation += 1;
+ this.ready = false;
+ this.initProcess = null;
+ this.failed = reason;
+ if (this.worker != null) {
+ this.worker.removeEventListener('message', this.onWorkerMessage);
+ this.worker.removeEventListener('error', this.onWorkerError);
+ this.worker.removeEventListener('messageerror', this.onWorkerMessageError);
+ this.worker.terminate();
+ this.worker = null;
+ }
+ if (this.workerUrl != null) {
+ URL.revokeObjectURL(this.workerUrl);
+ this.workerUrl = null;
+ }
+ for (const call of this.calls.values()) {
+ if (call.timer != null) clearTimeout(call.timer);
+ call.reject(new Error(reason));
+ }
+ this.calls.clear();
+ for (const list of this.waiters.values()) {
+ for (const waiter of list) {
+ if (waiter.timer != null) clearTimeout(waiter.timer);
+ waiter.reject(new Error(reason));
+ }
+ }
+ this.waiters.clear();
+ mark('worker-stopped', { reason, pendingCalls });
+ }
+ }
+
+ const initBridge = async (): Promise => {
+ mark('init-start');
+ postLoading(true);
+ const stdout: string[] = api.stdout ?? [];
+ const stderr: string[] = api.stderr ?? [];
+ api.stdout = stdout;
+ api.stderr = stderr;
+ try {
+ const bridge = await BeamBridge.init({
+ bundlePath: config.bundleUrl,
+ label: config.label,
+ mark,
+ onStdout: (text: string) => stdout.push(text),
+ onStderr: (text: string) => stderr.push(text),
+ onReady: async (readyBridge) => {
+ // Drop boot chatter (module import, fetches, wasm init) so it does not
+ // leak into the first run's output after a boot or crash recovery.
+ drain(stdout);
+ drain(stderr);
+ mark('prelude-start');
+ const prelude = await readyBridge.call(['eval_elixir', config.prelude], {
+ timeoutMs: EVAL_TIMEOUT_MS,
+ });
+ mark('prelude-end');
+ const preludeText = normalizeResultText(prelude?.data);
+ const parsePrelude = config.parsePreludeResult ?? parseTaggedResult;
+ const preludeError = parsePrelude(preludeText).error;
+ if (preludeError != null) {
+ throw new Error(`${config.label} prelude failed: ${preludeError}`);
+ }
+ // Warnings emitted while compiling the prelude are not user output.
+ drain(stdout);
+ drain(stderr);
+ },
+ });
+ api.popcorn = bridge;
+ api.failed = false;
+ mark('init-ready');
+ } catch (err) {
+ // Surface anything the runtime reported before dying (e.g. bundle/AtomVM fetch failures).
+ if (stderr.length > 0) console.error(drain(stderr));
+ if (stdout.length > 0) console.log(drain(stdout));
+ api.failed = true;
+ api.error = getErrorMessage(err);
+ mark('init-failed', { error: api.error });
+ throw err;
+ } finally {
+ postLoading(false);
+ }
+ };
+
+ // `ensureInit` is stored on the API object so it survives LiveReload
+ // document rewrites and can reuse the existing worker. A rejected attempt is
+ // cleared, so the next run (or retry()) starts a fresh boot instead of
+ // replaying a memoized failure forever.
+ api.ensureInit ??= async () => {
+ const existingBridge = api.popcorn as BeamBridge | undefined;
+ if (existingBridge != null) {
+ await existingBridge.ensureReady();
+ return;
+ }
+ if (api.init == null) {
+ const attempt = initBridge();
+ api.init = attempt;
+ attempt.catch(() => {
+ if (api.init === attempt) api.init = undefined;
+ });
+ }
+ await api.init;
+ };
+
+ const runOnce = async (input?: string, runId?: number): Promise => {
+ if (input !== undefined) api.input = input;
+ const stdin = `${input ?? api.input ?? ''}`;
+ const isLatest = () => runId == null || runId === api.runId;
+ api.output = null;
+ api.error = null;
+ api.exitCode = null;
+ api.value = null;
+ api.warnings = null;
+ api.ready = false;
+
+ const code = readCode();
+ mark('run-start', { runId, codeBytes: code.length, inputBytes: stdin.length });
+ if (!code.trim()) {
+ mark('run-empty', { runId });
+ if (isLatest()) {
+ api.output = null;
+ api.error = null;
+ api.exitCode = 0;
+ api.value = null;
+ api.warnings = null;
+ api.ready = true;
+ api.failed = false;
+ }
+ mark('run-end', {
+ runId,
+ exitCode: 0,
+ stdoutBytes: 0,
+ stderrBytes: 0,
+ hasError: false,
+ latest: isLatest(),
+ empty: true,
+ });
+ return { output: null, error: null, exitCode: 0, value: null, warnings: null };
+ }
+
+ try {
+ await api.ensureInit?.();
+ const bridge = api.popcorn as BeamBridge | undefined;
+ if (bridge == null) throw new Error(`${config.label} runtime is not initialized`);
+ api.stdout!.length = 0;
+ api.stderr!.length = 0;
+ mark('eval-start', { runId });
+ const { data } = await bridge.call(config.buildCall(code, stdin), {
+ timeoutMs: EVAL_TIMEOUT_MS,
+ });
+ const printed = drain(api.stdout!);
+ const errPrinted = drain(api.stderr!);
+ const parsed = config.parseResult(normalizeResult(data));
+ // The wrapper appends one newline to flush line-buffered IO.write/1. Drop
+ // exactly that newline; a single IO.puts chunk may include it.
+ const outputText =
+ stripTrailingNewline && printed.endsWith('\n') ? printed.slice(0, -1) : printed;
+ const output = outputText || null;
+ const warnings = errPrinted || null;
+ const latest = isLatest();
+ if (latest) {
+ api.output = output;
+ api.error = parsed.error;
+ api.exitCode = parsed.exitCode;
+ api.value = parsed.value;
+ api.warnings = warnings;
+ api.ready = true;
+ api.failed = false;
+ if (parsed.error != null) console.error(parsed.error);
+ if (output != null) console.log(output);
+ }
+ mark('run-end', {
+ runId,
+ exitCode: parsed.exitCode,
+ stdoutBytes: printed.length,
+ stderrBytes: errPrinted.length,
+ hasError: parsed.error != null,
+ latest,
+ });
+ return {
+ output,
+ error: parsed.error,
+ exitCode: parsed.exitCode,
+ value: parsed.value,
+ warnings,
+ ...(latest ? {} : { cancelled: true }),
+ };
+ } catch (err) {
+ const latest = isLatest();
+ const runtimeStderr = latest ? drain(api.stderr ?? []) : '';
+ const base = 'Error: ' + getErrorMessage(err);
+ const message = runtimeStderr.trim() ? `${base}\n${runtimeStderr.trim()}` : base;
+ if (latest) {
+ api.output = null;
+ api.error = message;
+ api.exitCode = 1;
+ api.value = null;
+ api.warnings = null;
+ api.ready = true;
+ api.failed = true;
+ console.error(message);
+ }
+ mark('run-error', { runId, error: message, latest });
+ return {
+ output: null,
+ error: message,
+ exitCode: 1,
+ value: null,
+ warnings: null,
+ ...(latest ? {} : { cancelled: true }),
+ };
+ }
+ };
+
+ let activeRun: Promise | null = null;
+ let pendingRun: {
+ input?: string;
+ runId: number;
+ resolve: (result: RunResult) => void;
+ reject: (reason?: unknown) => void;
+ } | null = null;
+ let supersedeTimer: ReturnType | null = null;
+
+ const clearSupersedeTimer = () => {
+ if (supersedeTimer == null) return;
+ clearTimeout(supersedeTimer);
+ supersedeTimer = null;
+ };
+
+ const scheduleSupersede = () => {
+ if (supersedeTimer != null) return;
+ supersedeTimer = setTimeout(() => {
+ supersedeTimer = null;
+ if (activeRun == null || pendingRun == null) return;
+ const bridge = api.popcorn as BeamBridge | undefined;
+ mark('run-supersede-timeout', { pendingRunId: pendingRun.runId });
+ bridge?.cancel('superseded by a newer run');
+ }, SUPERSEDE_TIMEOUT_MS);
+ };
+
+ const finishRun = (task: Promise) => {
+ if (activeRun !== task) return;
+ activeRun = null;
+ api.currentRun = null;
+ clearSupersedeTimer();
+ const next = pendingRun;
+ pendingRun = null;
+ if (next != null) {
+ mark('run-dequeued', { runId: next.runId });
+ const nextTask = startRun(next.input, next.runId);
+ void nextTask.then(next.resolve, next.reject);
+ }
+ };
+
+ const startRun = (input: string | undefined, runId: number): Promise => {
+ const task = runOnce(input, runId);
+ activeRun = task;
+ api.currentRun = task.then(
+ () => undefined,
+ () => undefined,
+ );
+ void task.then(
+ () => finishRun(task),
+ () => finishRun(task),
+ );
+ return task;
+ };
+
+ api.run ??= async (input?: string): Promise => {
+ const runId = (api.runId ?? 0) + 1;
+ api.runId = runId;
+ mark('run-queued', { runId, hasInput: input != null });
+ if (activeRun == null) {
+ return startRun(input, runId);
+ }
+ if (pendingRun != null) {
+ const superseded = pendingRun;
+ pendingRun = null;
+ mark('run-superseded', { runId: superseded.runId, by: runId });
+ superseded.resolve({
+ output: null,
+ error: null,
+ exitCode: null,
+ value: null,
+ warnings: null,
+ cancelled: true,
+ });
+ }
+ scheduleSupersede();
+ return new Promise((resolve, reject) => {
+ pendingRun = { input, runId, resolve, reject };
+ mark('run-pending', { runId });
+ });
+ };
+
+ api.retry ??= async () => {
+ api.failed = false;
+ api.error = null;
+ api.ready = false;
+ api.init = undefined;
+ const bridge = api.popcorn as BeamBridge | undefined;
+ try {
+ if (bridge != null) {
+ await bridge.restart();
+ } else {
+ await api.ensureInit?.();
+ }
+ } catch (err) {
+ api.failed = true;
+ api.error = getErrorMessage(err);
+ throw err;
+ }
+ };
+
+ // LiveReload updates rewrite the result document in rapid succession and the
+ // window `load` event is not reliably fired after coalesced rewrites, so runs
+ // are scheduled by polling the parser state instead of listening for `load`.
+ // Only the latest evaluation runs; superseded ones abort silently.
+ const waitForParseThenRun = async (evalId: number): Promise => {
+ const start = Date.now();
+ while (document.readyState === 'loading' && Date.now() - start < 30_000) {
+ if (api.evals !== evalId) return;
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ }
+ if (api.evals !== evalId) return;
+ postLoading(true);
+ try {
+ await api.run(api.input);
+ } finally {
+ postLoading(false);
+ }
+ };
+ void waitForParseThenRun(thisEval);
+
+ api.timings = timings;
+ const LOADED_TIMEOUT_MS = INIT_VM_TIMEOUT_MS + EVAL_TIMEOUT_MS;
+ api.loaded = new Promise((resolve, reject) => {
+ const started = performance.now();
+ const interval = setInterval(() => {
+ if (api.failed) {
+ clearInterval(interval);
+ mark('loaded-rejected', { error: api.error });
+ reject(new Error(api.error || `Failed to initialize the ${config.label} environment`));
+ } else if (api.ready) {
+ clearInterval(interval);
+ mark('loaded-resolved');
+ resolve();
+ } else if (performance.now() - started > LOADED_TIMEOUT_MS) {
+ clearInterval(interval);
+ api.failed = true;
+ api.error = `Timed out initializing the ${config.label} runtime`;
+ mark('loaded-timeout');
+ reject(new Error(api.error));
+ }
+ }, 50);
+ });
+ // The starter awaits `loaded` and may not catch. Attaching a no-op handler
+ // keeps a recoverable boot failure from becoming an unhandled rejection; the
+ // original promise still rejects for callers that want the error.
+ void api.loaded.catch(() => undefined);
+
+ window.livecodes[config.apiKey] = api;
+ if (config.apiAlias != null) {
+ window.livecodes[config.apiAlias] = api;
+ }
+ return api;
+};
diff --git a/src/livecodes/languages/elixir-wasm/__tests__/lang-elixir-wasm.spec.ts b/src/livecodes/languages/elixir-wasm/__tests__/lang-elixir-wasm.spec.ts
new file mode 100644
index 000000000..39e1ada97
--- /dev/null
+++ b/src/livecodes/languages/elixir-wasm/__tests__/lang-elixir-wasm.spec.ts
@@ -0,0 +1,72 @@
+import { readFileSync } from 'fs';
+import { resolve } from 'path';
+import { elixirWasm } from '../lang-elixir-wasm';
+
+const readSource = (file: string) =>
+ readFileSync(resolve(__dirname, '..', '..', 'beam-wasm', file), 'utf8');
+const readLanguageSource = (file: string) => readFileSync(resolve(__dirname, '..', file), 'utf8');
+
+describe('Elixir (Wasm) language spec', () => {
+ test('is registered as elixir-wasm, with the wasm compiler script', () => {
+ expect(elixirWasm.name).toBe('elixir-wasm');
+ expect(elixirWasm.title).toBe('Elixir (Wasm)');
+ expect(elixirWasm.compiler).toMatchObject({
+ scriptType: 'text/elixir-wasm',
+ compiledCodeLanguage: 'elixir',
+ liveReload: true,
+ });
+ });
+
+ test('accepts the common Elixir extensions', () => {
+ expect(elixirWasm.extensions).toEqual(
+ expect.arrayContaining(['ex', 'exs', 'elixir', 'elixir-wasm', 'wasm.ex']),
+ );
+ });
+
+ test('warns that the first run is a large download', () => {
+ expect(elixirWasm.largeDownload).toBe(true);
+ });
+
+ test('uses a code editor with Elixir support', () => {
+ expect(elixirWasm.editor).toBe('script');
+ expect(elixirWasm.editorSupport?.monaco?.language).toBe('elixir');
+ expect(elixirWasm.editorSupport?.codejar?.language).toBe('elixir');
+ });
+});
+
+describe('Elixir (Wasm) bridge', () => {
+ const bridge = readSource('runtime.ts');
+ const worker = readSource('lang-beam-wasm-worker.raw.js');
+ const script = readLanguageSource('lang-elixir-wasm-script.ts');
+
+ test('keeps the VM in a module worker instead of an iframe', () => {
+ expect(bridge).toContain("new Worker(url, { type: 'module' })");
+ expect(bridge).toContain('worker.terminate()');
+ expect(bridge).not.toContain("createElement('iframe')");
+ });
+
+ test('uses tagged results for values, errors, and exits', () => {
+ expect(bridge).toContain("OK_TAG = '__LC_OK__'");
+ expect(bridge).toContain("ERROR_TAG = '__LC_ERROR__'");
+ expect(bridge).toContain("EXIT_TAG = '__LC_EXIT__'");
+ expect(script).toContain('Base.encode64');
+ });
+
+ test('bounds calls and supersedes a blocked run', () => {
+ expect(bridge).toContain('EVAL_TIMEOUT_MS = 60_000');
+ expect(bridge).toContain('SUPERSEDE_TIMEOUT_MS = 10_000');
+ expect(bridge).toContain("mark('run-supersede-timeout'");
+ expect(bridge).toContain('const latest = isLatest()');
+ });
+
+ test('boots AtomVM in a worker with the required interop shim', () => {
+ expect(worker).toContain("INIT: 'popcorn-init'");
+ expect(worker).toContain("CALL: 'popcorn-call'");
+ expect(worker).toContain('globalThis.window = globalThis');
+ expect(worker).toContain('noInitialRun: true');
+ expect(worker).toContain("Module.callMain(['/data/bundle.avm'])");
+ expect(worker).toContain('Module.onElixirReady');
+ expect(worker).toContain('self.postMessage');
+ expect(worker).not.toContain('window.parent.postMessage');
+ });
+});
diff --git a/src/livecodes/languages/elixir-wasm/index.ts b/src/livecodes/languages/elixir-wasm/index.ts
new file mode 100644
index 000000000..ed93b98ef
--- /dev/null
+++ b/src/livecodes/languages/elixir-wasm/index.ts
@@ -0,0 +1 @@
+export * from './lang-elixir-wasm';
diff --git a/src/livecodes/languages/elixir-wasm/lang-elixir-wasm-script.ts b/src/livecodes/languages/elixir-wasm/lang-elixir-wasm-script.ts
new file mode 100644
index 000000000..10a9463ac
--- /dev/null
+++ b/src/livecodes/languages/elixir-wasm/lang-elixir-wasm-script.ts
@@ -0,0 +1,74 @@
+import { atomvmWasmBaseUrl } from '../../vendors';
+import { BEAM_PRELUDE } from '../beam-wasm/prelude';
+import { createBeamRuntime, parseTaggedResult, type ParsedResult } from '../beam-wasm/runtime';
+
+const BUNDLE_URL = `${atomvmWasmBaseUrl}bundle.avm`;
+const SCRIPT_TYPE = 'text/elixir-wasm';
+
+const escapeElixirString = (value: string) =>
+ value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/#{/g, '#\\{');
+
+const wrapElixir = (code: string, input: string) => {
+ // The user source is embedded as a string and parsed first with
+ // Code.string_to_quoted/1: a syntax error becomes a tagged value instead of
+ // an exception that would wedge (or kill) the VM with no reply.
+ const source = escapeElixirString(code);
+ const stdin = escapeElixirString(input);
+ return `livecodes_input = "${stdin}"
+livecodes_source = "${source}"
+gl = Process.group_leader()
+dev = :livecodes_io.start(livecodes_input)
+Process.group_leader(self(), dev)
+result =
+ try do
+ case Code.string_to_quoted(livecodes_source) do
+ {:error, {meta, message, _token}} ->
+ line = if is_list(meta), do: Keyword.get(meta, :line, 0), else: 0
+ {:lc_error, :syntax, "SyntaxError: " <> to_string(message) <> " (line " <> to_string(line) <> ")", ""}
+ {:error, err} ->
+ {:lc_error, :syntax, "SyntaxError: " <> inspect(err), ""}
+ {:ok, _} ->
+ try do
+ {value, _binding} = Code.eval_string(livecodes_source, livecodes_input: livecodes_input)
+ {:lc_ok, value}
+ rescue
+ e -> {:lc_error, :error, inspect(e.__struct__) <> ": " <> Exception.message(e), ""}
+ catch
+ :throw, {:lc_exit, status} -> {:lc_exit, status}
+ kind, other -> {:lc_error, kind, inspect(other), ""}
+ end
+ end
+ after
+ Process.group_leader(self(), gl)
+ # Terminals are line buffered: a final newline flushes an IO.write/1 that
+ # did not end in a newline. The main thread drops the resulting trailing
+ # newline so the observable output does not change.
+ IO.write("\\n")
+ end
+:livecodes_io.stop(dev)
+case result do
+ {:lc_ok, value} ->
+ text = inspect(value)
+ text = if byte_size(text) > 1500, do: binary_part(text, 0, 1500) <> "...", else: text
+ "__LC_OK__" <> Base.encode64(text)
+ {:lc_error, kind, message, _stack} ->
+ message = if byte_size(message) > 1000, do: binary_part(message, 0, 1000) <> "...", else: message
+ "__LC_ERROR__" <> Base.encode64(to_string(kind)) <> "|" <> Base.encode64(message)
+ {:lc_exit, status} ->
+ "__LC_EXIT__" <> Integer.to_string(status)
+end`;
+};
+
+const parseElixirResult = (text: string): ParsedResult => parseTaggedResult(text, 'Elixir error');
+
+createBeamRuntime({
+ apiKey: 'elixir',
+ apiAlias: 'elixir-wasm',
+ label: 'Elixir',
+ scriptType: SCRIPT_TYPE,
+ bundleUrl: BUNDLE_URL,
+ prelude: BEAM_PRELUDE,
+ buildCall: (code, stdin) => ['eval_elixir', wrapElixir(code, stdin)],
+ parseResult: parseElixirResult,
+ parsePreludeResult: parseElixirResult,
+});
diff --git a/src/livecodes/languages/elixir-wasm/lang-elixir-wasm.ts b/src/livecodes/languages/elixir-wasm/lang-elixir-wasm.ts
new file mode 100644
index 000000000..9a7137be0
--- /dev/null
+++ b/src/livecodes/languages/elixir-wasm/lang-elixir-wasm.ts
@@ -0,0 +1,29 @@
+import { codemirrorLegacy } from '../../editor/codemirror/utils';
+import type { LanguageSpecs } from '../../models';
+import { codeMirrorBaseUrl, monacoLanguagesBaseUrl } from '../../vendors';
+
+export const elixirWasm: LanguageSpecs = {
+ name: 'elixir-wasm',
+ title: 'Elixir (Wasm)',
+ compiler: {
+ factory: () => async (code) => code,
+ scripts: ({ baseUrl }) => [baseUrl + '{{hash:lang-elixir-wasm-script.js}}'],
+ scriptType: 'text/elixir-wasm',
+ compiledCodeLanguage: 'elixir',
+ liveReload: true,
+ },
+ extensions: ['ex', 'exs', 'elixir', 'elixir-wasm', 'wasm.ex'],
+ editor: 'script',
+ editorSupport: {
+ monaco: {
+ languageSupport: monacoLanguagesBaseUrl + 'elixir.js',
+ language: 'elixir',
+ },
+ codemirror: {
+ languageSupport: async () =>
+ codemirrorLegacy((await import(codeMirrorBaseUrl + 'codemirror-lang-ruby.js')).ruby),
+ },
+ codejar: { language: 'elixir' },
+ },
+ largeDownload: true,
+};
diff --git a/src/livecodes/languages/erlang-wasm/__tests__/lang-erlang-wasm.spec.ts b/src/livecodes/languages/erlang-wasm/__tests__/lang-erlang-wasm.spec.ts
new file mode 100644
index 000000000..e117276d2
--- /dev/null
+++ b/src/livecodes/languages/erlang-wasm/__tests__/lang-erlang-wasm.spec.ts
@@ -0,0 +1,56 @@
+import { readFileSync } from 'fs';
+import { resolve } from 'path';
+import { erlangWasm } from '../lang-erlang-wasm';
+
+const readSource = (file: string) => readFileSync(resolve(__dirname, '..', file), 'utf8');
+
+describe('Erlang (Wasm) language spec', () => {
+ test('is registered as erlang-wasm, with the wasm compiler script', () => {
+ expect(erlangWasm.name).toBe('erlang-wasm');
+ expect(erlangWasm.title).toBe('Erlang (Wasm)');
+ expect(erlangWasm.compiler).toMatchObject({
+ scriptType: 'text/erlang-wasm',
+ compiledCodeLanguage: 'erlang',
+ liveReload: true,
+ });
+ });
+
+ test('accepts the common Erlang extensions', () => {
+ expect(erlangWasm.extensions).toEqual(
+ expect.arrayContaining(['erl', 'erlang', 'erlang-wasm', 'wasm.erl']),
+ );
+ });
+
+ test('warns that the first run is a large download', () => {
+ expect(erlangWasm.largeDownload).toBe(true);
+ });
+
+ test('uses a code editor with Erlang support', () => {
+ expect(erlangWasm.editor).toBe('script');
+ expect(erlangWasm.editorSupport?.monaco?.language).toBe('erlang');
+ expect(erlangWasm.editorSupport?.codejar?.language).toBe('erlang');
+ });
+});
+
+describe('Erlang (Wasm) runtime', () => {
+ const script = readSource('lang-erlang-wasm-script.ts');
+
+ test('reuses the shared beam runtime', () => {
+ expect(script).toContain("from '../beam-wasm/runtime'");
+ expect(script).toContain("from '../beam-wasm/prelude'");
+ expect(script).toContain("apiKey: 'erlang'");
+ expect(script).toContain("apiAlias: 'erlang-wasm'");
+ });
+
+ test('wraps expressions and compiles modules', () => {
+ expect(script).toContain("'eval_erlang_module'");
+ expect(script).toContain('wrapErlang');
+ expect(script).toContain('stripTrailingDot');
+ expect(script).toContain("replace(/\\berlang:halt\\(/g, 'livecodes_io:halt(')");
+ });
+
+ test('keeps the Erlang output newline and inspected result text', () => {
+ expect(script).toContain('stripTrailingNewline: false');
+ expect(script).toContain('normalizeResult: (data) =>');
+ });
+});
diff --git a/src/livecodes/languages/erlang-wasm/index.ts b/src/livecodes/languages/erlang-wasm/index.ts
new file mode 100644
index 000000000..7d3d70555
--- /dev/null
+++ b/src/livecodes/languages/erlang-wasm/index.ts
@@ -0,0 +1 @@
+export * from './lang-erlang-wasm';
diff --git a/src/livecodes/languages/erlang-wasm/lang-erlang-wasm-script.ts b/src/livecodes/languages/erlang-wasm/lang-erlang-wasm-script.ts
new file mode 100644
index 000000000..bbdca0fbe
--- /dev/null
+++ b/src/livecodes/languages/erlang-wasm/lang-erlang-wasm-script.ts
@@ -0,0 +1,64 @@
+import { atomvmWasmBaseUrl } from '../../vendors';
+import { BEAM_PRELUDE } from '../beam-wasm/prelude';
+import { createBeamRuntime, parseTaggedResult, type ParsedResult } from '../beam-wasm/runtime';
+
+const BUNDLE_URL = `${atomvmWasmBaseUrl}bundle.avm`;
+const SCRIPT_TYPE = 'text/erlang-wasm';
+
+const erlangEscapes: Record = {
+ '\\': '\\\\',
+ '"': '\\"',
+ '\n': '\\n',
+ '\r': '\\r',
+ '\t': '\\t',
+};
+
+// An Erlang string literal is a charlist; `:livecodes_io.start/1` normalizes it.
+const escapeErlangString = (value: string) =>
+ value.replace(/[\\"\n\r\t]/g, (char) => erlangEscapes[char] ?? char);
+
+const stripTrailingDot = (code: string) => code.replace(/\.(\s*(?:%[^\n]*)?\s*)$/, '$1');
+
+const wrapErlang = (code: string, input: string) => {
+ // `erlang:halt/1` is a NIF this build does not have, so route it through the
+ // shared device. A bare `halt/0-1` is rewritten too, as in upstream Popcorn.
+ // The final expression dot is removed so the code can be spliced into `try`.
+ const source = code
+ .replace(/\berlang:halt\(/g, 'livecodes_io:halt(')
+ .replace(/(? {lc_exit, LcStatus}; LcKind:LcReason -> {lc_error, {LcKind, LcReason}} end,
+group_leader(LcGL, self()),
+livecodes_io:stop(LcDev),
+LcValue.`;
+};
+
+// Erlang modules are compiled and loaded as written, so they stay callable in
+// later runs; expressions are wrapped for stdin, exit codes and error handling.
+const actionFor = (code: string) =>
+ code.startsWith('-module(') ? 'eval_erlang_module' : 'eval_erlang';
+
+const parseErlangResult = (text: string): ParsedResult => parseTaggedResult(text, 'Erlang error');
+
+createBeamRuntime({
+ apiKey: 'erlang',
+ apiAlias: 'erlang-wasm',
+ label: 'Erlang',
+ scriptType: SCRIPT_TYPE,
+ bundleUrl: BUNDLE_URL,
+ prelude: BEAM_PRELUDE,
+ buildCall: (code, stdin) => {
+ const action = actionFor(code);
+ return [action, action === 'eval_erlang_module' ? code : wrapErlang(code, stdin)];
+ },
+ parseResult: parseErlangResult,
+ parsePreludeResult: (text) => parseTaggedResult(text, 'Elixir prelude error'),
+ // Unlike the Elixir wrapper, the Erlang wrapper does not append a newline.
+ stripTrailingNewline: false,
+ // The runtime returns the inspected term as a string; keep it as-is.
+ normalizeResult: (data) => (data == null ? '' : String(data)),
+});
diff --git a/src/livecodes/languages/erlang-wasm/lang-erlang-wasm.ts b/src/livecodes/languages/erlang-wasm/lang-erlang-wasm.ts
new file mode 100644
index 000000000..848880414
--- /dev/null
+++ b/src/livecodes/languages/erlang-wasm/lang-erlang-wasm.ts
@@ -0,0 +1,29 @@
+import { codemirrorLegacy } from '../../editor/codemirror/utils';
+import type { LanguageSpecs } from '../../models';
+import { codeMirrorBaseUrl, monacoLanguagesBaseUrl } from '../../vendors';
+
+export const erlangWasm: LanguageSpecs = {
+ name: 'erlang-wasm',
+ title: 'Erlang (Wasm)',
+ compiler: {
+ factory: () => async (code) => code,
+ scripts: ({ baseUrl }) => [baseUrl + '{{hash:lang-erlang-wasm-script.js}}'],
+ scriptType: 'text/erlang-wasm',
+ compiledCodeLanguage: 'erlang',
+ liveReload: true,
+ },
+ extensions: ['erl', 'erlang', 'erlang-wasm', 'wasm.erl'],
+ editor: 'script',
+ editorSupport: {
+ monaco: {
+ languageSupport: monacoLanguagesBaseUrl + 'erlang.js',
+ language: 'erlang',
+ },
+ codemirror: {
+ languageSupport: async () =>
+ codemirrorLegacy((await import(codeMirrorBaseUrl + 'codemirror-lang-erlang.js')).erlang),
+ },
+ codejar: { language: 'erlang' },
+ },
+ largeDownload: true,
+};
diff --git a/src/livecodes/languages/languages.ts b/src/livecodes/languages/languages.ts
index 09cf7fdcf..efc553e2a 100644
--- a/src/livecodes/languages/languages.ts
+++ b/src/livecodes/languages/languages.ts
@@ -27,7 +27,9 @@ import { diagrams } from './diagrams';
import { dot } from './dot';
import { duckdb } from './duckdb';
import { ejs } from './ejs';
+import { elixirWasm } from './elixir-wasm';
import { elm } from './elm';
+import { erlangWasm } from './erlang-wasm';
import { eta } from './eta';
import { fanak } from './fanak';
import { fennel } from './fennel';
@@ -207,6 +209,8 @@ export const languages: LanguageSpecs[] = [
fsharp,
fsharpWasm,
fanak,
+ elixirWasm,
+ erlangWasm,
haskell,
haskellWasm,
scheme,
diff --git a/src/livecodes/models.ts b/src/livecodes/models.ts
index ef9cc9632..d966b1636 100644
--- a/src/livecodes/models.ts
+++ b/src/livecodes/models.ts
@@ -243,6 +243,8 @@ export type ScriptType =
| 'text/swift-wasm'
| 'text/ada-wasm'
| 'text/v-wasm'
+ | 'text/elixir-wasm'
+ | 'text/erlang-wasm'
| 'text/assembly'
| 'application/json'
| 'application/lua'
diff --git a/src/livecodes/templates/starter/elixir-wasm-starter.ts b/src/livecodes/templates/starter/elixir-wasm-starter.ts
new file mode 100644
index 000000000..659bbb29d
--- /dev/null
+++ b/src/livecodes/templates/starter/elixir-wasm-starter.ts
@@ -0,0 +1,90 @@
+import type { Template } from '../../models';
+
+export const elixirWasmStarter: Template = {
+ name: 'elixir-wasm',
+ title: window.deps.translateString('templates.starter.elixir-wasm', 'Elixir Starter'),
+ thumbnail: 'assets/templates/elixir.png',
+ 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: 'elixir-wasm',
+ content: `
+count =
+ case livecodes_input do
+ "" -> 0
+ value -> String.to_integer(String.trim(value))
+ end
+
+IO.puts("Elixir")
+IO.puts(count + 1)
+`.trimStart(),
+ },
+};
diff --git a/src/livecodes/templates/starter/erlang-wasm-starter.ts b/src/livecodes/templates/starter/erlang-wasm-starter.ts
new file mode 100644
index 000000000..b89cc4662
--- /dev/null
+++ b/src/livecodes/templates/starter/erlang-wasm-starter.ts
@@ -0,0 +1,89 @@
+import type { Template } from '../../models';
+
+export const erlangWasmStarter: Template = {
+ name: 'erlang-wasm',
+ title: window.deps.translateString('templates.starter.erlang-wasm', 'Erlang Starter'),
+ thumbnail: 'assets/templates/erlang.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: 'erlang-wasm',
+ content: `
+Count =
+ case io:get_line("") of
+ eof -> 0;
+ Value -> binary_to_integer(binary:replace(Value, <<"\\n">>, <<>>, [global]))
+ end,
+io:format("Erlang~n"),
+io:format("~p~n", [Count + 1]).
+`.trimStart(),
+ },
+};
diff --git a/src/livecodes/templates/starter/index.ts b/src/livecodes/templates/starter/index.ts
index a3d668b49..e1da2a56a 100644
--- a/src/livecodes/templates/starter/index.ts
+++ b/src/livecodes/templates/starter/index.ts
@@ -27,7 +27,9 @@ import { daisyuiStarter } from './daisyui-starter';
import { dartStarter } from './dart-starter';
import { diagramsStarter } from './diagrams-starter';
import { duckdbStarter } from './duckdb-starter';
+import { elixirWasmStarter } from './elixir-wasm-starter';
import { elmStarter } from './elm-starter';
+import { erlangWasmStarter } from './erlang-wasm-starter';
import { fanakStarter } from './fanak-starter';
import { fennelStarter } from './fennel-starter';
import { flutterStarter } from './flutter-starter';
@@ -160,6 +162,8 @@ export const starterTemplates = [
cobolWasmStarter,
fortranWasmStarter,
dWasmStarter,
+ elixirWasmStarter,
+ erlangWasmStarter,
vWasmStarter,
rustWasmStarter,
scalaStarter,
diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts
index 486c16fb1..fbee98971 100644
--- a/src/livecodes/vendors.ts
+++ b/src/livecodes/vendors.ts
@@ -152,6 +152,10 @@ export const duckdbWasmModuleUrl = /* @__PURE__ */ getModuleUrl('@duckdb/duckdb-
export const ejsUrl = /* @__PURE__ */ getUrl('ejs@4.0.1/ejs.js');
+export const atomvmWasmBaseUrl = /* @__PURE__ */ getUrl(
+ 'gh:live-codes/browser-elixir@e9bd2faed8eb6e2bcacd4b42f8b57ea26a770831/runtime/wasm/',
+);
+
export const elkjsBaseUrl = /* @__PURE__ */ getUrl('elkjs@0.8.2/lib/');
export const elmWasmBaseUrl = /* @__PURE__ */ getUrl('@live-codes/elm-wasm@0.2.1/dist/');
diff --git a/src/sdk/models.ts b/src/sdk/models.ts
index 3f581849f..5e17b1ba7 100644
--- a/src/sdk/models.ts
+++ b/src/sdk/models.ts
@@ -253,6 +253,15 @@ export type Language =
| 'wasm.fsx'
| 'fanak'
| 'fnk'
+ | 'elixir'
+ | 'ex'
+ | 'exs'
+ | 'elixir-wasm'
+ | 'wasm.ex'
+ | 'erlang'
+ | 'erl'
+ | 'erlang-wasm'
+ | 'wasm.erl'
| 'rust'
| 'rs'
| 'rust-wasm'
@@ -541,6 +550,8 @@ export type TemplateName =
| 'odin-wasm'
| 'cobol-wasm'
| 'v-wasm'
+ | 'elixir-wasm'
+ | 'erlang-wasm'
| 'java'
| 'csharp-wasm'
| 'blazor-wasm'
@@ -975,6 +986,8 @@ export type CustomSettings = Partial<
| 'text/go-wasm'
| 'text/zig-wasm'
| 'text/v-wasm'
+ | 'text/elixir-wasm'
+ | 'text/erlang-wasm'
| 'application/json'
| 'application/lua'
| 'text/fennel'
diff --git a/storybook/_stories/EmbedOptions/template.ts b/storybook/_stories/EmbedOptions/template.ts
index bc044c84d..002fb262a 100644
--- a/storybook/_stories/EmbedOptions/template.ts
+++ b/storybook/_stories/EmbedOptions/template.ts
@@ -61,6 +61,8 @@ const storyDef: StoryDef = {
CobolWasm: { props: { template: 'cobol-wasm' }, storyName: 'COBOL (Wasm)' },
FortranWasm: { props: { template: 'fortran-wasm' }, storyName: 'Fortran (Wasm)' },
VWasm: { props: { template: 'v-wasm' } },
+ ElixirWasm: { props: { template: 'elixir-wasm' } },
+ ErlangWasm: { props: { template: 'erlang-wasm' } },
RustWasm: { props: { template: 'rust-wasm' } },
Scala: { props: { template: 'scala' } },
SwiftWasm: { props: { template: 'swift-wasm' } },
diff --git a/storybook/preact/stories/EmbedOptions/template.stories.ts b/storybook/preact/stories/EmbedOptions/template.stories.ts
index 994740ac4..3b0c2c8a1 100644
--- a/storybook/preact/stories/EmbedOptions/template.stories.ts
+++ b/storybook/preact/stories/EmbedOptions/template.stories.ts
@@ -79,6 +79,8 @@ CobolWasm.storyName = 'COBOL (Wasm)';
export const FortranWasm = livecodesStory({ template: 'fortran-wasm' });
FortranWasm.storyName = 'Fortran (Wasm)';
export const VWasm = livecodesStory({ template: 'v-wasm' });
+export const ElixirWasm = livecodesStory({ template: 'elixir-wasm' });
+export const ErlangWasm = livecodesStory({ template: 'erlang-wasm' });
export const RustWasm = livecodesStory({ template: 'rust-wasm' });
export const Scala = livecodesStory({ template: 'scala' });
export const SwiftWasm = livecodesStory({ template: 'swift-wasm' });
diff --git a/storybook/react/stories/EmbedOptions/template.stories.ts b/storybook/react/stories/EmbedOptions/template.stories.ts
index 994740ac4..3b0c2c8a1 100644
--- a/storybook/react/stories/EmbedOptions/template.stories.ts
+++ b/storybook/react/stories/EmbedOptions/template.stories.ts
@@ -79,6 +79,8 @@ CobolWasm.storyName = 'COBOL (Wasm)';
export const FortranWasm = livecodesStory({ template: 'fortran-wasm' });
FortranWasm.storyName = 'Fortran (Wasm)';
export const VWasm = livecodesStory({ template: 'v-wasm' });
+export const ElixirWasm = livecodesStory({ template: 'elixir-wasm' });
+export const ErlangWasm = livecodesStory({ template: 'erlang-wasm' });
export const RustWasm = livecodesStory({ template: 'rust-wasm' });
export const Scala = livecodesStory({ template: 'scala' });
export const SwiftWasm = livecodesStory({ template: 'swift-wasm' });
diff --git a/storybook/solid/stories/EmbedOptions/template.stories.ts b/storybook/solid/stories/EmbedOptions/template.stories.ts
index 994740ac4..3b0c2c8a1 100644
--- a/storybook/solid/stories/EmbedOptions/template.stories.ts
+++ b/storybook/solid/stories/EmbedOptions/template.stories.ts
@@ -79,6 +79,8 @@ CobolWasm.storyName = 'COBOL (Wasm)';
export const FortranWasm = livecodesStory({ template: 'fortran-wasm' });
FortranWasm.storyName = 'Fortran (Wasm)';
export const VWasm = livecodesStory({ template: 'v-wasm' });
+export const ElixirWasm = livecodesStory({ template: 'elixir-wasm' });
+export const ErlangWasm = livecodesStory({ template: 'erlang-wasm' });
export const RustWasm = livecodesStory({ template: 'rust-wasm' });
export const Scala = livecodesStory({ template: 'scala' });
export const SwiftWasm = livecodesStory({ template: 'swift-wasm' });
diff --git a/storybook/svelte/stories/EmbedOptions/template.stories.ts b/storybook/svelte/stories/EmbedOptions/template.stories.ts
index 994740ac4..3b0c2c8a1 100644
--- a/storybook/svelte/stories/EmbedOptions/template.stories.ts
+++ b/storybook/svelte/stories/EmbedOptions/template.stories.ts
@@ -79,6 +79,8 @@ CobolWasm.storyName = 'COBOL (Wasm)';
export const FortranWasm = livecodesStory({ template: 'fortran-wasm' });
FortranWasm.storyName = 'Fortran (Wasm)';
export const VWasm = livecodesStory({ template: 'v-wasm' });
+export const ElixirWasm = livecodesStory({ template: 'elixir-wasm' });
+export const ErlangWasm = livecodesStory({ template: 'erlang-wasm' });
export const RustWasm = livecodesStory({ template: 'rust-wasm' });
export const Scala = livecodesStory({ template: 'scala' });
export const SwiftWasm = livecodesStory({ template: 'swift-wasm' });
diff --git a/storybook/vue/stories/EmbedOptions/template.stories.ts b/storybook/vue/stories/EmbedOptions/template.stories.ts
index 994740ac4..3b0c2c8a1 100644
--- a/storybook/vue/stories/EmbedOptions/template.stories.ts
+++ b/storybook/vue/stories/EmbedOptions/template.stories.ts
@@ -79,6 +79,8 @@ CobolWasm.storyName = 'COBOL (Wasm)';
export const FortranWasm = livecodesStory({ template: 'fortran-wasm' });
FortranWasm.storyName = 'Fortran (Wasm)';
export const VWasm = livecodesStory({ template: 'v-wasm' });
+export const ElixirWasm = livecodesStory({ template: 'elixir-wasm' });
+export const ErlangWasm = livecodesStory({ template: 'erlang-wasm' });
export const RustWasm = livecodesStory({ template: 'rust-wasm' });
export const Scala = livecodesStory({ template: 'scala' });
export const SwiftWasm = livecodesStory({ template: 'swift-wasm' });
diff --git a/storybook/web-components/stories/EmbedOptions/template.stories.ts b/storybook/web-components/stories/EmbedOptions/template.stories.ts
index 994740ac4..3b0c2c8a1 100644
--- a/storybook/web-components/stories/EmbedOptions/template.stories.ts
+++ b/storybook/web-components/stories/EmbedOptions/template.stories.ts
@@ -79,6 +79,8 @@ CobolWasm.storyName = 'COBOL (Wasm)';
export const FortranWasm = livecodesStory({ template: 'fortran-wasm' });
FortranWasm.storyName = 'Fortran (Wasm)';
export const VWasm = livecodesStory({ template: 'v-wasm' });
+export const ElixirWasm = livecodesStory({ template: 'elixir-wasm' });
+export const ErlangWasm = livecodesStory({ template: 'erlang-wasm' });
export const RustWasm = livecodesStory({ template: 'rust-wasm' });
export const Scala = livecodesStory({ template: 'scala' });
export const SwiftWasm = livecodesStory({ template: 'swift-wasm' });
diff --git a/tsconfig.json b/tsconfig.json
index fa9076966..a2bcbf7eb 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -55,7 +55,8 @@
"src/sdk/react.tsx",
"src/livecodes/languages/fsharp-wasm/lang-fsharp-wasm-worker.raw.js",
"src/livecodes/languages/haskell-wasm/lang-haskell-wasm-worker.raw.js",
- "src/livecodes/languages/vb-wasm/lang-vb-wasm-worker.raw.js"
+ "src/livecodes/languages/vb-wasm/lang-vb-wasm-worker.raw.js",
+ "src/livecodes/languages/elixir-wasm/lang-elixir-wasm-worker.raw.js"
],
"exclude": ["**/node_modules/**", "src/livecodes/i18n/locales/**"]
}
diff --git a/vendor-licenses.md b/vendor-licenses.md
index 9ebb167fe..f3a997b23 100644
--- a/vendor-licenses.md
+++ b/vendor-licenses.md
@@ -384,6 +384,16 @@ LFortran (Wasm): [BSD 3-Clause License](https://github.com/lfortran/lfortran/blo
V (Wasm): [MIT License](https://github.com/vlang/v/blob/master/LICENSE) (the V compiler and its standard library, compiled to WebAssembly by @live-codes/v-wasm; the Clang toolchain it links through is covered by LLVM / LLD below)
+@live-codes/browser-elixir: [MIT License](https://github.com/live-codes/browser-elixir/blob/main/LICENSE) (the build scripts and checksums for the single-threaded AtomVM WebAssembly runtime and bytecode bundle used by Elixir (Wasm) and Erlang (Wasm); the artifacts those scripts produce are built from AtomVM and Popcorn, both Apache-2.0 and listed below)
+
+Elixir (Wasm): [Apache License 2.0](https://github.com/elixir-lang/elixir/blob/main/LICENSE) (the Elixir 1.17.3 compiler and standard library, packaged for browser execution)
+
+Elixir logo: registered trademark of The Elixir Team, used under [nominative use](https://elixir-lang.org/trademarks/) to identify the supported language.
+
+Popcorn: [Apache License 2.0](https://github.com/software-mansion/popcorn/blob/main/LICENSE) (the eval server cooked into the bytecode bundle, and the browser bridge the worker runtime derives from)
+
+AtomVM: [Apache License 2.0](https://github.com/atomvm/AtomVM/blob/main/LICENSE) (the lightweight Erlang VM running the compiled Elixir code in WebAssembly; the Emscripten build comes from [FissionVM](https://github.com/software-mansion-labs/FissionVM), also Apache-2.0)
+
@live-codes/janet-wasm: [MIT License](https://github.com/live-codes/browser-janet/blob/main/packages/janet-wasm/LICENSE)
Janet (Wasm): [MIT License](https://github.com/janet-lang/janet/blob/master/LICENSE) (the Janet interpreter, compiled to WebAssembly by @live-codes/janet-wasm)