feat(lua): standalone EdgeTX Lua compiler (#7275)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a672c3c5c6
commit
65f135c25a
+61
@@ -0,0 +1,61 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(edgetx-luac C)
|
||||
|
||||
# Native host build of luac that produces bytecode compatible with
|
||||
# EdgeTX's 32-bit Lua 5.3 configuration (float numbers, int integers).
|
||||
#
|
||||
# Usage:
|
||||
# cmake -S radio/src/thirdparty/Lua -B build-luac
|
||||
# cmake --build build-luac
|
||||
#
|
||||
# The resulting 'edgetx-luac' binary can compile scripts for any EdgeTX radio:
|
||||
# edgetx-luac -s -o script.luac script.lua
|
||||
|
||||
set(LUA_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||
|
||||
set(LUA_CORE_SRC
|
||||
${LUA_DIR}/lapi.c
|
||||
${LUA_DIR}/lcode.c
|
||||
${LUA_DIR}/lctype.c
|
||||
${LUA_DIR}/ldebug.c
|
||||
${LUA_DIR}/ldo.c
|
||||
${LUA_DIR}/ldump.c
|
||||
${LUA_DIR}/lfunc.c
|
||||
${LUA_DIR}/lgc.c
|
||||
${LUA_DIR}/llex.c
|
||||
${LUA_DIR}/lmem.c
|
||||
${LUA_DIR}/lobject.c
|
||||
${LUA_DIR}/lopcodes.c
|
||||
${LUA_DIR}/lparser.c
|
||||
${LUA_DIR}/lstate.c
|
||||
${LUA_DIR}/lstring.c
|
||||
${LUA_DIR}/ltable.c
|
||||
${LUA_DIR}/ltm.c
|
||||
${LUA_DIR}/lundump.c
|
||||
${LUA_DIR}/lvm.c
|
||||
${LUA_DIR}/lzio.c
|
||||
${LUA_DIR}/lauxlib.c
|
||||
)
|
||||
|
||||
add_executable(edgetx-luac
|
||||
${LUA_DIR}/luac.c
|
||||
${LUA_CORE_SRC}
|
||||
)
|
||||
|
||||
target_compile_definitions(edgetx-luac PRIVATE
|
||||
LUA_HOST_BUILD
|
||||
LUA_CROSS_COMPILER
|
||||
LUA_COMPAT_5_2
|
||||
)
|
||||
|
||||
target_include_directories(edgetx-luac PRIVATE ${LUA_DIR})
|
||||
|
||||
# Link math library on Unix
|
||||
if(UNIX)
|
||||
target_link_libraries(edgetx-luac m)
|
||||
endif()
|
||||
|
||||
set_target_properties(edgetx-luac PROPERTIES
|
||||
OUTPUT_NAME edgetx-luac
|
||||
C_STANDARD 99
|
||||
)
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Build edgetx-luac.wasm — a browser-ready WASM module that compiles
|
||||
# Lua source to EdgeTX-compatible bytecode entirely in memory.
|
||||
#
|
||||
# Requirements: wasi-sdk (default /opt/wasi-sdk)
|
||||
#
|
||||
# Usage:
|
||||
# cd radio/src/thirdparty/Lua
|
||||
# ./build-wasm.sh
|
||||
#
|
||||
# The output edgetx-luac.wasm is placed in the current directory.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
WASI_SDK="${WASI_SDK:-/opt/wasi-sdk}"
|
||||
CC="${WASI_SDK}/bin/clang"
|
||||
|
||||
if [ ! -x "$CC" ]; then
|
||||
echo "Error: wasi-sdk clang not found at $CC" >&2
|
||||
echo "Set WASI_SDK to your wasi-sdk installation path." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SRCDIR="$(cd "$(dirname "$0")/src" && pwd)"
|
||||
|
||||
LUA_CORE_SRC=(
|
||||
"$SRCDIR/lapi.c"
|
||||
"$SRCDIR/lcode.c"
|
||||
"$SRCDIR/lctype.c"
|
||||
"$SRCDIR/ldebug.c"
|
||||
"$SRCDIR/ldo.c"
|
||||
"$SRCDIR/ldump.c"
|
||||
"$SRCDIR/lfunc.c"
|
||||
"$SRCDIR/lgc.c"
|
||||
"$SRCDIR/llex.c"
|
||||
"$SRCDIR/lmem.c"
|
||||
"$SRCDIR/lobject.c"
|
||||
"$SRCDIR/lopcodes.c"
|
||||
"$SRCDIR/lparser.c"
|
||||
"$SRCDIR/lstate.c"
|
||||
"$SRCDIR/lstring.c"
|
||||
"$SRCDIR/ltable.c"
|
||||
"$SRCDIR/ltm.c"
|
||||
"$SRCDIR/lundump.c"
|
||||
"$SRCDIR/lvm.c"
|
||||
"$SRCDIR/lzio.c"
|
||||
"$SRCDIR/lauxlib.c"
|
||||
)
|
||||
|
||||
echo "Building edgetx-luac.wasm..."
|
||||
|
||||
"$CC" \
|
||||
--target=wasm32-wasi \
|
||||
--sysroot="${WASI_SDK}/share/wasi-sysroot" \
|
||||
-Os \
|
||||
-ffunction-sections \
|
||||
-fdata-sections \
|
||||
-mllvm -wasm-enable-sjlj \
|
||||
-DLUA_HOST_BUILD \
|
||||
-DLUA_CROSS_COMPILER \
|
||||
-DLUA_COMPAT_5_2 \
|
||||
-I"$SRCDIR" \
|
||||
-mexec-model=reactor \
|
||||
-Wl,--gc-sections \
|
||||
-Wl,--strip-debug \
|
||||
-Wl,--export-dynamic \
|
||||
-Wl,-mllvm,-wasm-enable-sjlj \
|
||||
-nodefaultlibs -lc -lsetjmp \
|
||||
-o edgetx-luac.wasm \
|
||||
"$SRCDIR/luac_wasm.c" \
|
||||
"${LUA_CORE_SRC[@]}"
|
||||
|
||||
SIZE=$(wc -c < edgetx-luac.wasm | tr -d ' ')
|
||||
echo "Done: edgetx-luac.wasm (${SIZE} bytes)"
|
||||
|
||||
# Show WASI imports (ideally very few / none needed at runtime)
|
||||
if command -v wasm-objdump &>/dev/null; then
|
||||
echo ""
|
||||
echo "WASI imports:"
|
||||
wasm-objdump -j Import -x edgetx-luac.wasm 2>/dev/null | grep wasi || echo " (none)"
|
||||
fi
|
||||
Vendored
+3
-2
@@ -9,7 +9,9 @@
|
||||
|
||||
#include "lprefix.h"
|
||||
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
#include <alloca.h>
|
||||
#endif
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
@@ -21,7 +23,6 @@
|
||||
#include "lualib.h"
|
||||
#include "lauxlib.h"
|
||||
#include "ldebug.h"
|
||||
#include "lnodemcu.h"
|
||||
#include "lobject.h"
|
||||
#include "lstate.h"
|
||||
#include "lstring.h"
|
||||
@@ -163,7 +164,7 @@ static int pmain(lua_State *L) {
|
||||
if (argc == 0)
|
||||
return 0;
|
||||
for (i = 0; i < argc; i++) {
|
||||
const char *filename = IS("-") ? NULL : filelist[i];
|
||||
const char *filename = IS("-") ? NULL : argv[i];
|
||||
if (luaL_loadfile(L, filename) != LUA_OK)
|
||||
fatal(lua_tostring(L, -1));
|
||||
}
|
||||
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
** EdgeTX Lua cross-compiler — WASM entry point
|
||||
**
|
||||
** Exposes a minimal API for compiling Lua source to bytecode entirely
|
||||
** in memory, suitable for running in a browser via WebAssembly.
|
||||
**
|
||||
** Exported functions:
|
||||
** compile(src_ptr, src_len, strip) → positive length on success, negative on error
|
||||
** get_output_ptr() → pointer to compiled bytecode
|
||||
** get_error_ptr() → pointer to error message string
|
||||
** get_error_len() → length of error message
|
||||
** wasm_malloc(size) → allocate memory (for JS to write source into)
|
||||
** wasm_free(ptr) → free allocated memory
|
||||
*/
|
||||
|
||||
#define luac_c
|
||||
#define LUA_CORE
|
||||
|
||||
#include "lprefix.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "lua.h"
|
||||
#include "lauxlib.h"
|
||||
|
||||
/* --------------- output buffer --------------- */
|
||||
|
||||
static unsigned char *out_buf;
|
||||
static size_t out_len;
|
||||
static size_t out_cap;
|
||||
|
||||
static int writer(lua_State *L, const void *p, size_t size, void *ud) {
|
||||
(void)L; (void)ud;
|
||||
if (out_len + size > out_cap) {
|
||||
size_t new_cap = out_cap ? out_cap * 2 : 4096;
|
||||
while (new_cap < out_len + size)
|
||||
new_cap *= 2;
|
||||
unsigned char *nb = realloc(out_buf, new_cap);
|
||||
if (!nb) return 1;
|
||||
out_buf = nb;
|
||||
out_cap = new_cap;
|
||||
}
|
||||
memcpy(out_buf + out_len, p, size);
|
||||
out_len += size;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --------------- error message --------------- */
|
||||
|
||||
static char error_msg[512];
|
||||
static int error_len;
|
||||
|
||||
/* --------------- exported API --------------- */
|
||||
|
||||
#define EXPORT __attribute__((export_name(#name)))
|
||||
/* clang wasm export attribute helper */
|
||||
#define WASM_EXPORT(name) __attribute__((export_name(#name))) name
|
||||
|
||||
/*
|
||||
** Compile Lua source to bytecode.
|
||||
** Returns the bytecode length (>0) on success, or a negative error code:
|
||||
** -1 could not create Lua state
|
||||
** -2 syntax / compile error (call get_error_ptr/len for message)
|
||||
** -3 bytecode dump failed
|
||||
*/
|
||||
int WASM_EXPORT(compile)(const char *source, int source_len, int strip) {
|
||||
error_msg[0] = '\0';
|
||||
error_len = 0;
|
||||
|
||||
/* reset output */
|
||||
free(out_buf);
|
||||
out_buf = NULL;
|
||||
out_len = 0;
|
||||
out_cap = 0;
|
||||
|
||||
lua_State *L = luaL_newstate();
|
||||
if (!L) return -1;
|
||||
|
||||
int status = luaL_loadbuffer(L, source, (size_t)source_len, "input");
|
||||
if (status != LUA_OK) {
|
||||
const char *err = lua_tostring(L, -1);
|
||||
if (err) {
|
||||
size_t n = strlen(err);
|
||||
if (n >= sizeof(error_msg)) n = sizeof(error_msg) - 1;
|
||||
memcpy(error_msg, err, n);
|
||||
error_msg[n] = '\0';
|
||||
error_len = (int)n;
|
||||
}
|
||||
lua_close(L);
|
||||
return -2;
|
||||
}
|
||||
|
||||
/* The compiled chunk is a function at the top of the stack.
|
||||
* lua_dump serialises it via the writer callback. */
|
||||
status = lua_dump(L, writer, NULL, strip);
|
||||
lua_close(L);
|
||||
|
||||
return status == 0 ? (int)out_len : -3;
|
||||
}
|
||||
|
||||
unsigned char *WASM_EXPORT(get_output_ptr)(void) {
|
||||
return out_buf;
|
||||
}
|
||||
|
||||
char *WASM_EXPORT(get_error_ptr)(void) {
|
||||
return error_msg;
|
||||
}
|
||||
|
||||
int WASM_EXPORT(get_error_len)(void) {
|
||||
return error_len;
|
||||
}
|
||||
|
||||
void *WASM_EXPORT(wasm_malloc)(int size) {
|
||||
return malloc((size_t)size);
|
||||
}
|
||||
|
||||
void WASM_EXPORT(wasm_free)(void *ptr) {
|
||||
free(ptr);
|
||||
}
|
||||
+2
@@ -462,10 +462,12 @@
|
||||
#undef lua_str2number
|
||||
#define lua_str2number(s,p) ((lua_Number)strtod((s), (p)))
|
||||
|
||||
#if !defined(LUA_HOST_BUILD)
|
||||
// For print
|
||||
#include "debug.h"
|
||||
#define lua_writestring(s,l) TRACE_DEBUG_WP("%s", s);
|
||||
#define lua_writeline() TRACE_DEBUG_WP("\n");
|
||||
#define lua_writestringerror(s,p) TRACE_DEBUG_WP(s, p);
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
.wrangler
|
||||
*.wasm
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
+342
@@ -0,0 +1,342 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>EdgeTX Lua Compiler</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&family=Roboto+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--primary: #0C7BCD;
|
||||
--primary-dark: #0a6ab3;
|
||||
--bg: #1c1c1c;
|
||||
--surface: #2a2a2a;
|
||||
--surface-light: #333;
|
||||
--border: #444;
|
||||
--text: #f0f0f0;
|
||||
--text-dim: #999;
|
||||
--success: #4ade80;
|
||||
--error: #f87171;
|
||||
--mono: "Roboto Mono", "SF Mono", Consolas, monospace;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: "Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.header img {
|
||||
height: 40px;
|
||||
width: auto;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.subtitle {
|
||||
color: var(--text-dim);
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
}
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
font-family: var(--mono);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
resize: vertical;
|
||||
tab-size: 2;
|
||||
}
|
||||
textarea:focus { outline: 1px solid var(--primary); border-color: var(--primary); }
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin: 0.75rem 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
button {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.25rem;
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
button:hover { background: var(--primary-dark); }
|
||||
button:disabled { opacity: 0.4; cursor: not-allowed; background: var(--primary); }
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-dim);
|
||||
cursor: pointer;
|
||||
}
|
||||
input[type="checkbox"] { accent-color: var(--primary); }
|
||||
.file-input-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
.file-input-wrapper input[type="file"] {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.file-input-wrapper .btn-file {
|
||||
background: var(--surface-light);
|
||||
border: 1px solid var(--border);
|
||||
pointer-events: none;
|
||||
}
|
||||
.file-input-wrapper .btn-file:hover { background: var(--surface-light); }
|
||||
#status {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-family: var(--mono);
|
||||
font-size: 0.8rem;
|
||||
white-space: pre-wrap;
|
||||
display: none;
|
||||
}
|
||||
#status.error { display: block; background: #2d1515; color: var(--error); border: 1px solid #5c2020; }
|
||||
#status.success { display: block; background: #0d2818; color: var(--success); border: 1px solid #1a4028; }
|
||||
#status.info { display: block; background: var(--surface); color: var(--text-dim); border: 1px solid var(--border); }
|
||||
.drop-target {
|
||||
outline: 2px dashed var(--primary);
|
||||
outline-offset: -4px;
|
||||
}
|
||||
footer {
|
||||
margin-top: auto;
|
||||
padding-top: 2rem;
|
||||
color: var(--text-dim);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
footer a { color: var(--primary); text-decoration: none; }
|
||||
footer a:hover { text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="header">
|
||||
<img src="edgetx-logo.png" alt="EdgeTX">
|
||||
<h1>Lua Compiler</h1>
|
||||
</div>
|
||||
<p class="subtitle">Compile Lua scripts to EdgeTX bytecode — runs entirely in your browser</p>
|
||||
|
||||
<div class="container">
|
||||
<textarea id="source" spellcheck="false" placeholder="-- Paste or drop your Lua script here...
|
||||
|
||||
local function hello()
|
||||
print('Hello from EdgeTX!')
|
||||
end
|
||||
|
||||
return { run = hello }
|
||||
"></textarea>
|
||||
|
||||
<div class="toolbar">
|
||||
<button id="compile-btn" disabled>Loading WASM...</button>
|
||||
<label><input type="checkbox" id="strip" checked> Strip debug info</label>
|
||||
<div class="file-input-wrapper">
|
||||
<input type="file" id="file-input" accept=".lua,.txt">
|
||||
<button class="btn-file">Open file...</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="status"></div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
Powered by <a href="https://github.com/EdgeTX/edgetx">EdgeTX</a> Lua 5.3
|
||||
— compiled to WebAssembly, no server needed
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const sourceEl = document.getElementById("source");
|
||||
const compileBtn = document.getElementById("compile-btn");
|
||||
const stripEl = document.getElementById("strip");
|
||||
const statusEl = document.getElementById("status");
|
||||
const fileInput = document.getElementById("file-input");
|
||||
|
||||
let wasm = null; // WASM instance exports
|
||||
let currentFileName = "script.lua"; // track loaded filename
|
||||
|
||||
/* ---- WASI stubs (never actually called at runtime) ---- */
|
||||
const wasiStubs = {
|
||||
args_get: () => 0,
|
||||
args_sizes_get: () => 0,
|
||||
clock_time_get: () => 0,
|
||||
fd_close: () => 0,
|
||||
fd_prestat_get: () => 8, /* EBADF — no preopens */
|
||||
fd_prestat_dir_name: () => 8,
|
||||
fd_read: () => 0,
|
||||
fd_seek: () => 0,
|
||||
fd_write: (fd, iovs, iovs_len, nwritten) => {
|
||||
/* minimal fd_write so Lua error messages via fprintf(stderr,...) don't crash */
|
||||
const mem = new DataView(wasm.memory.buffer);
|
||||
let written = 0;
|
||||
for (let i = 0; i < iovs_len; i++) {
|
||||
const ptr = mem.getUint32(iovs + i * 8, true);
|
||||
const len = mem.getUint32(iovs + i * 8 + 4, true);
|
||||
written += len;
|
||||
}
|
||||
mem.setUint32(nwritten, written, true);
|
||||
return 0;
|
||||
},
|
||||
proc_exit: (code) => { throw new Error("proc_exit(" + code + ")"); },
|
||||
random_get: () => 0,
|
||||
};
|
||||
|
||||
/* ---- Load WASM module ---- */
|
||||
async function loadWasm() {
|
||||
const importObject = {
|
||||
wasi_snapshot_preview1: wasiStubs,
|
||||
};
|
||||
const resp = await fetch("edgetx-luac.wasm");
|
||||
const { instance } = await WebAssembly.instantiateStreaming(resp, importObject);
|
||||
wasm = instance.exports;
|
||||
|
||||
/* reactor model: call _initialize once */
|
||||
if (wasm._initialize) wasm._initialize();
|
||||
|
||||
compileBtn.textContent = "Compile";
|
||||
compileBtn.disabled = false;
|
||||
}
|
||||
|
||||
/* ---- Compile ---- */
|
||||
function compile() {
|
||||
const source = sourceEl.value;
|
||||
if (!source.trim()) {
|
||||
showStatus("error", "No Lua source to compile.");
|
||||
return;
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const srcBytes = encoder.encode(source);
|
||||
|
||||
/* allocate WASM memory and copy source in */
|
||||
const srcPtr = wasm.wasm_malloc(srcBytes.length);
|
||||
if (!srcPtr) {
|
||||
showStatus("error", "Failed to allocate WASM memory.");
|
||||
return;
|
||||
}
|
||||
|
||||
const mem = new Uint8Array(wasm.memory.buffer);
|
||||
mem.set(srcBytes, srcPtr);
|
||||
|
||||
const strip = stripEl.checked ? 1 : 0;
|
||||
const result = wasm.compile(srcPtr, srcBytes.length, strip);
|
||||
wasm.wasm_free(srcPtr);
|
||||
|
||||
if (result > 0) {
|
||||
/* success — read output bytecode */
|
||||
const outPtr = wasm.get_output_ptr();
|
||||
const bytecode = new Uint8Array(wasm.memory.buffer, outPtr, result).slice();
|
||||
|
||||
/* trigger download — use original filename with .luac extension */
|
||||
const name = currentFileName.replace(/\.[^.]+$/, "") + ".luac";
|
||||
const blob = new Blob([bytecode], { type: "application/octet-stream" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = name;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
showStatus("success", "Compiled successfully (" + bytecode.length + " bytes) — downloading " + name);
|
||||
} else {
|
||||
/* error — read message */
|
||||
const errLen = wasm.get_error_len();
|
||||
let msg = "Compilation failed (code " + result + ")";
|
||||
if (errLen > 0) {
|
||||
const errPtr = wasm.get_error_ptr();
|
||||
const errBytes = new Uint8Array(wasm.memory.buffer, errPtr, errLen);
|
||||
msg = new TextDecoder().decode(errBytes);
|
||||
}
|
||||
showStatus("error", msg);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- UI helpers ---- */
|
||||
function showStatus(type, msg) {
|
||||
statusEl.className = type;
|
||||
statusEl.textContent = msg;
|
||||
}
|
||||
|
||||
function loadFile(file) {
|
||||
currentFileName = file.name;
|
||||
file.text().then((text) => {
|
||||
sourceEl.value = text;
|
||||
showStatus("info", "Loaded " + file.name + " (" + text.length + " bytes)");
|
||||
});
|
||||
}
|
||||
|
||||
compileBtn.addEventListener("click", compile);
|
||||
|
||||
/* File input */
|
||||
fileInput.addEventListener("change", (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
loadFile(file);
|
||||
fileInput.value = ""; /* allow re-uploading the same file */
|
||||
});
|
||||
|
||||
/* Drag & drop */
|
||||
sourceEl.addEventListener("dragover", (e) => {
|
||||
e.preventDefault();
|
||||
sourceEl.classList.add("drop-target");
|
||||
});
|
||||
sourceEl.addEventListener("dragleave", () => {
|
||||
sourceEl.classList.remove("drop-target");
|
||||
});
|
||||
sourceEl.addEventListener("drop", (e) => {
|
||||
e.preventDefault();
|
||||
sourceEl.classList.remove("drop-target");
|
||||
const file = e.dataTransfer.files[0];
|
||||
if (file) loadFile(file);
|
||||
});
|
||||
|
||||
/* Ctrl/Cmd+Enter to compile */
|
||||
sourceEl.addEventListener("keydown", (e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
if (!compileBtn.disabled) compile();
|
||||
}
|
||||
});
|
||||
|
||||
/* Boot */
|
||||
loadWasm().catch((err) => {
|
||||
showStatus("error", "Failed to load WASM module: " + err.message);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user