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>