Commitf6eb66eaRecorded31 Mar 2026Repositorysigil-web-repl

Replace Emscripten runtime with WASI polyfill

Message

Use @bjorn3/browserwasishim from CDN for WASI syscall polyfill instead of Emscripten's Module system. Dynamic import() keeps everything in a single script scope. Loads compiled modules via lib-manifest.json into a virtual filesystem.

Changed
 assets/index.html | 192 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------------------------------------------------------------------
 1 file changed, 97 insertions(+), 95 deletions(-)
Diff
assets/index.htmlmodified
@@ -974,6 +974,17 @@
974
updateHighlight();
975
}
976
});
+977
</script>
+978
<script type="module">
+979
import { WASI, File, Directory, OpenFile, ConsoleStdout, PreopenDirectory }
+980
from "https://cdn.jsdelivr.net/npm/@bjorn3/[email protected]/dist/index.js";
+981
+982
const mainContainer = document.getElementById('main-container');
+983
const terminalContainer = document.getElementById('terminal-container');
+984
const loading = document.getElementById('loading');
+985
const editor = document.getElementById('editor');
+986
const runBtn = document.getElementById('run-btn');
+987
const stopBtn = document.getElementById('stop-btn');
988
989
// Create xterm.js terminal
990
const term = new Terminal({
@@ -992,51 +1003,38 @@
1003
const fitAddon = new FitAddon.FitAddon();
1004
term.loadAddon(fitAddon);
1005
995
// Line input state
+1006
// State
1007
let currentLine = '';
1008
let history = [];
1009
let historyIndex = -1;
1010
let ready = false;
1011
let waitingForInput = false;
1012
let running = false;
1002
1003
// WASM functions
1013
let sigilInput = null;
1014
let sigilEval = null;
1015
1016
// Run code from editor
1017
function runCode() {
1018
if (!ready) return;
1010
1019
const code = editor.value.trim();
1020
if (!code) return;
1021
1022
running = true;
1023
runBtn.disabled = true;
1024
stopBtn.disabled = false;
1017
1018
// Add spacing before output
1025
term.write('\r\n');
1026
1021
// Evaluate the whole file at once
1027
sigilEval(code);
1028
1024
// Flush any remaining output (setTimeout won't fire until we return to event loop)
1025
flushStdout();
1026
flushStderr();
1027
1029
running = false;
1030
runBtn.disabled = false;
1031
stopBtn.disabled = true;
1032
1032
// Send empty input to trigger REPL prompt
1033
if (waitingForInput) {
1034
sigilInput('');
1035
flushStdout();
1035
}
1036
}
1037
1039
// Stop execution (limited - can only stop between expressions)
1038
function stopCode() {
1039
running = false;
1040
runBtn.disabled = false;
@@ -1047,6 +1045,7 @@
1045
runBtn.addEventListener('click', runCode);
1046
stopBtn.addEventListener('click', stopCode);
1047
+1048
// Terminal input handling
1049
function handleInput(data) {
1050
if (!ready || !waitingForInput) return;
1051
@@ -1056,18 +1055,14 @@
1055
1056
if (code === 13) { // Enter
1057
term.write('\r\n');
1059
1058
if (currentLine.trim()) {
1059
history.push(currentLine);
1060
historyIndex = history.length;
1061
}
1064
1062
const line = currentLine;
1063
currentLine = '';
1067
1064
waitingForInput = false;
1065
const status = sigilInput(line);
1070
1066
if (status === 1) {
1067
waitingForInput = true;
1068
} else if (status === 0) {
@@ -1112,9 +1107,7 @@
1107
currentLine = '';
1108
waitingForInput = false;
1109
const status = sigilInput('');
1115
if (status === 1) {
1116
waitingForInput = true;
1117
}
+1110
if (status === 1) waitingForInput = true;
1111
} else if (code >= 32) { // Printable character
1112
currentLine += char;
1113
term.write(char);
@@ -1122,80 +1115,93 @@
1115
}
1116
}
1117
1125
// UTF-8 decoders with stream mode for handling incomplete sequences
1126
const stdoutDecoder = new TextDecoder('utf-8', { fatal: false });
1127
const stderrDecoder = new TextDecoder('utf-8', { fatal: false });
1128
let stdoutBytes = [];
1129
let stderrBytes = [];
1130
let stdoutFlushPending = false;
1131
let stderrFlushPending = false;
1132
1133
function flushStdout() {
1134
stdoutFlushPending = false;
1135
if (stdoutBytes.length > 0) {
1136
const text = stdoutDecoder.decode(new Uint8Array(stdoutBytes), { stream: true });
1137
stdoutBytes = [];
1138
if (text && ready) {
1139
term.write(text.replace(/\n/g, '\r\n'));
1140
} else if (text) {
1141
console.log(text);
1142
}
+1118
term.onData(handleInput);
+1119
window.addEventListener('resize', () => { if (ready) fitAddon.fit(); });
+1120
+1121
// Build virtual filesystem from lib/ directory
+1122
async function buildFs() {
+1123
const { File, Directory } = await import("https://cdn.jsdelivr.net/npm/@bjorn3/[email protected]/dist/index.js");
+1124
const files = {};
+1125
try {
+1126
const resp = await fetch('lib-manifest.json');
+1127
if (!resp.ok) return { files, File, Directory };
+1128
const manifest = await resp.json();
+1129
await Promise.all(manifest.map(async (path) => {
+1130
const fileResp = await fetch('lib/' + path);
+1131
if (fileResp.ok) {
+1132
const data = new Uint8Array(await fileResp.arrayBuffer());
+1133
const parts = path.split('/');
+1134
let dir = files;
+1135
for (let i = 0; i < parts.length - 1; i++) {
+1136
if (!dir[parts[i]]) dir[parts[i]] = {};
+1137
dir = dir[parts[i]];
+1138
}
+1139
dir[parts[parts.length - 1]] = new File(data);
+1140
}
+1141
}));
+1142
} catch (e) {
+1143
console.warn('Module loading:', e.message);
1144
}
+1145
return { files, File, Directory };
1146
}
1147
1146
function flushStderr() {
1147
stderrFlushPending = false;
1148
if (stderrBytes.length > 0) {
1149
const text = stderrDecoder.decode(new Uint8Array(stderrBytes), { stream: true });
1150
stderrBytes = [];
1151
if (text && ready) {
1152
term.write(text.replace(/\n/g, '\r\n'));
1153
} else if (text) {
1154
console.error(text);
+1148
function makeDir(obj, FileClass, DirectoryClass) {
+1149
const entries = new Map();
+1150
for (const [name, value] of Object.entries(obj)) {
+1151
if (value instanceof FileClass) {
+1152
entries.set(name, value);
+1153
} else {
+1154
entries.set(name, makeDir(value, FileClass, DirectoryClass));
1155
}
1156
}
+1157
return new DirectoryClass(entries);
1158
}
1159
1159
function scheduleStdoutFlush() {
1160
if (!stdoutFlushPending) {
1161
stdoutFlushPending = true;
1162
setTimeout(flushStdout, 0);
1163
}
1164
}
1165
1166
function scheduleStderrFlush() {
1167
if (!stderrFlushPending) {
1168
stderrFlushPending = true;
1169
setTimeout(flushStderr, 0);
1170
}
+1160
function callWithString(instance, fn, str) {
+1161
const encoder = new TextEncoder();
+1162
const bytes = encoder.encode(str + '\0');
+1163
const ptr = instance.exports.malloc(bytes.length);
+1164
new Uint8Array(instance.exports.memory.buffer, ptr, bytes.length).set(bytes);
+1165
const result = fn(ptr);
+1166
instance.exports.free(ptr);
+1167
return result;
1168
}
1169
1173
// Module configuration
1174
var Module = {
1175
preRun: [],
1176
postRun: [],
1177
stdout: function(charCode) {
1178
if (charCode === 0) return;
1179
stdoutBytes.push(charCode);
1180
if (charCode === 10) {
1181
flushStdout();
1182
} else {
1183
scheduleStdoutFlush();
1184
}
1185
},
1186
stderr: function(charCode) {
1187
if (charCode === 0) return;
1188
stderrBytes.push(charCode);
1189
if (charCode === 10) {
1190
flushStderr();
1191
} else {
1192
scheduleStderrFlush();
1193
}
1194
},
1195
onRuntimeInitialized: function() {
1196
sigilInput = Module.cwrap('sigil_web_input', 'number', ['string']);
1197
sigilEval = Module.cwrap('sigil_web_eval', 'number', ['string']);
1198
+1170
// Initialize WASI and load WASM
+1171
async function init() {
+1172
const { WASI, File: WasiFile, OpenFile, ConsoleStdout, PreopenDirectory } =
+1173
await import("https://cdn.jsdelivr.net/npm/@bjorn3/[email protected]/dist/index.js");
+1174
+1175
const { files: libFiles, File: FileClass, Directory: DirectoryClass } = await buildFs();
+1176
+1177
const fds = [
+1178
new OpenFile(new WasiFile([])), // stdin
+1179
ConsoleStdout.lineBuffered(msg => {
+1180
term.write(msg.replace(/\n/g, '\r\n') + '\r\n');
+1181
}),
+1182
ConsoleStdout.lineBuffered(msg => {
+1183
term.write(msg.replace(/\n/g, '\r\n') + '\r\n');
+1184
}),
+1185
new PreopenDirectory("/", new Map([
+1186
["lib", makeDir(libFiles, FileClass, DirectoryClass)]
+1187
])),
+1188
];
+1189
+1190
const wasi = new WASI(["sigil-web"], [], fds);
+1191
+1192
try {
+1193
const wasmModule = await WebAssembly.compileStreaming(fetch('sigil-web-repl.wasm'));
+1194
const instance = await WebAssembly.instantiate(wasmModule, {
+1195
wasi_snapshot_preview1: wasi.wasiImport,
+1196
});
+1197
+1198
sigilInput = (str) => callWithString(instance, instance.exports.sigil_web_input, str);
+1199
sigilEval = (str) => callWithString(instance, instance.exports.sigil_web_eval, str);
+1200
+1201
// Start the VM
+1202
wasi.start(instance);
+1203
+1204
// Show UI
1205
loading.style.display = 'none';
1206
document.getElementById('header').style.display = 'flex';
1207
mainContainer.style.display = 'flex';
@@ -1205,17 +1211,13 @@
1211
ready = true;
1212
waitingForInput = true;
1213
term.focus();
+1214
} catch (e) {
+1215
loading.textContent = 'Error: ' + e.message;
+1216
console.error(e);
1217
}
1209
};
1210
1211
term.onData(handleInput);
+1218
}
1219
1213
window.addEventListener('resize', function() {
1214
if (ready) {
1215
fitAddon.fit();
1216
}
1217
});
+1220
init();
1221
</script>
1219
<script async src="sigil-web-repl.js"></script>
1222
</body>
1223
</html>