Multiple implementations (JS, Wasm, C) of a Lisp.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

308 lines
8.6 KiB

import {Repl} from './repl.js';
let buffer = '';
const input = document.querySelector('#hidden-input');
const history = document.querySelector('#history');
const text = document.querySelector('#text');
const prefix = document.querySelector('#prefix');
const canvas = document.querySelector('#canvas');
const canvasSwitch = document.querySelector('#canvas-switch');
let showCanvas = false;
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const clearScreen = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx.clearRect(0, 0, canvas.width, canvas.height);
};
const fillRect = (color, x, y, w, h) => {
ctx.fillStyle = color;
ctx.fillRect(x, y, w, h);
};
canvasSwitch.addEventListener('click', () => {
showCanvas = !showCanvas;
if (showCanvas) {
canvas.style.display = 'block';
canvasSwitch.textContent = 'REPL';
} else {
canvas.style.display = 'none';
canvasSwitch.textContent = 'CANVAS';
}
})
const repl = new Repl(loadFromFile, loadTest, async () => {
const onwrite = repl.stdout.onwrite;
prefix.innerHTML = '<br>';
const text = await read('<br>');
repl.stdout.onwrite = result => {
history.innerHTML += `${result.replaceAll('\n', '<br>')}`;
repl.stdout.onwrite = onwrite;
};
prefix.textContent = '';
repl.stdin.pushData(text);
});
function evalp(text) {
if (!text.trim()) {
return false;
}
return Array.from(text).reduce((op, c) => {
if (c === '(') {
return op + 1;
}
if (c === ')' && op > 0) {
return op - 1;
}
return op;
}, 0) === 0;
}
function read(prefix = '') {
return new Promise(resolve => {
const controller = new AbortController();
input.addEventListener(
'keypress',
(e) => {
switch(e.key) {
case 'Enter':
if (evalp(input.value) &&
(input.selectionStart == input.value.length || e.shiftKey)) {
let text = input.value;
history.innerHTML += `${prefix}${text}`;
input.value = '';
updateText();
resolve(text);
e.preventDefault();
controller.abort();
}
break;
}
},
{ signal: controller.signal })
});
}
function loadFromFile() {
return new Promise((resolve) => {
const input = document.createElement('input');
input.setAttribute('type', 'file');
input.setAttribute('accept', '.ptlisp');
input.setAttribute('class', 'hidden');
document.body.appendChild(input);
input.addEventListener('change', e => {
const reader = new FileReader();
reader.addEventListener('load', e => {
resolve(e.target.result)
document.body.removeChild(input);
});
reader.readAsText(e.target.files[0]);
});
input.click();
});
}
async function loadTest() {
let response = await fetch("test.ptlisp");
if (response.status != 200) {
throw new Error("Server Error");
}
return await response.text();
}
function readInput() {
prefix.textContent = 'USER > ';
read(prefix.textContent).then(text => {
prefix.textContent = '';
repl.stdout.onwrite = result => {
history.innerHTML += `${result.replaceAll('\n', '<br>')}`;
readInput();
};
repl.stdin.pushData(text);
});
}
readInput();
function getNextBracket (chars, searched, alternate) {
let alternates = 0;
let i = 0;
for (let c of chars) {
if (alternates == 0 && c == searched) {
return i;
} else if (c == alternate) {
alternates++;
} else if (c == searched) {
alternates--;
}
i++;
}
}
function getNextClosingBracket(text, index) {
return getNextBracket(Array.from(text.substr(index)), ')', '(') + index;
}
function getPreviousOpenBracket(text, index) {
return index - getNextBracket(Array.from(text.substr(0, index)).reverse(), '(', ')') - 1;
}
function updateText(selectionStart, selectionEnd) {
while(text.firstChild) {
text.removeChild(text.lastChild);
}
selectionEnd += selectionEnd - selectionStart ? 0 : 1;
let cursorText = input.value.substr(selectionStart, selectionEnd - selectionStart) || '&nbsp;';
let secondCursorIdx = -1;
switch (cursorText) {
case '(': secondCursorIdx = getNextClosingBracket(input.value, selectionEnd); break;
case ')': secondCursorIdx = getPreviousOpenBracket(input.value, selectionStart); break;
}
if (secondCursorIdx >= 0 && selectionStart > secondCursorIdx) {
text.appendChild(document.createTextNode(input.value.substr(0, secondCursorIdx)));
const secondCursor = document.createElement('SPAN');
secondCursor.classList.add('second-cursor');
secondCursor.appendChild(document.createTextNode(input.value.substr(secondCursorIdx, 1)));
text.appendChild(secondCursor);
if (secondCursorIdx + 1 != selectionStart) {
text.appendChild(
document.createTextNode(
input.value.substr(secondCursorIdx + 1, selectionStart - (secondCursorIdx + 1))));
}
} else if (selectionStart > 0) {
text.appendChild(document.createTextNode(input.value.substr(0, selectionStart)));
}
let cursor = document.createElement('SPAN');
cursor.classList.add('cursor');
// cursor.appendChild(document.createTextNode(cursorText));
cursor.innerHTML = cursorText;
if (cursorText == '\n') {
cursor.classList.add('end-of-line');
}
text.appendChild(cursor);
if (secondCursorIdx >= selectionEnd) {
text.appendChild(
document.createTextNode(
input.value.substr(selectionEnd, secondCursorIdx - selectionEnd)));
const secondCursor = document.createElement('SPAN');
secondCursor.classList.add('second-cursor');
secondCursor.appendChild(document.createTextNode(input.value.substr(secondCursorIdx, 1)));
text.appendChild(secondCursor);
text.appendChild(
document.createTextNode(
input.value.substr(secondCursorIdx + 1)));
} else if (selectionEnd < input.value.length) {
text.appendChild(document.createTextNode(input.value.substr(selectionEnd)));
}
}
input.addEventListener('input', (e) => {
var selection = input.selectionStart;
if (e.data == '(') {
input.value = input.value.slice(0, selection) + ')' + input.value.slice(selection);
input.setSelectionRange(selection, selection);
}
updateText(input.selectionStart, input.selectionEnd);
buffer = input.value;
})
input.addEventListener('keydown', (e) => {
if (e.key == 'Backspace' &&
input.selectionEnd == input.selectionStart) {
if (input.value.substr(input.selectionStart - 1, 1) == '(' &&
input.value.substr(input.selectionStart, 1) == ')') {
input.value = input.value.substr(0, input.selectionStart - 1) + input.value.substr(input.selectionStart + 1);
input.selectionStart = input.selectionEnd = input.selectionStart - 1;
e.preventDefault();
} else if (input.value.substr(input.selectionStart - 1, 1) == ')'){
input.selectionStart = input.selectionEnd = input.selectionStart - 1;
e.preventDefault();
}
} else if (e.key == ')' &&
input.selectionStart - input.selectionEnd == 0 &&
input.value.substr(input.selectionStart, 1) == ')') {
input.selectionEnd = ++input.selectionStart;
e.preventDefault();
}
setTimeout(() => {
updateText(input.selectionStart, input.selectionEnd);
});
})
document.addEventListener('mouseup', (e) => {
const selection = document.getSelection();
if (!(selection.anchorNode.parentNode == text ||
selection.anchorNode.parentNode.parentNode == text)) {
return;
}
let textNodes = [], node, walk = document.createTreeWalker(text, NodeFilter.SHOW_TEXT, null, false);
while (node = walk.nextNode()) {
textNodes.push(node);
}
let startNodeIdx = textNodes.indexOf(selection.anchorNode);
let endNodeIdx = textNodes.indexOf(selection.focusNode);
let startNodeOffset = selection.anchorOffset;
let endNodeOffset = selection.focusOffset;
for (let i = 0; i < startNodeIdx; i++) {
startNodeOffset += textNodes[i].textContent.length;
}
for (let i = 0; i < endNodeIdx; i++) {
endNodeOffset += textNodes[i].textContent.length;
}
input.setSelectionRange(Math.min(startNodeOffset, endNodeOffset),
Math.max(startNodeOffset, endNodeOffset));
updateText(input.selectionStart, input.selectionEnd);
});
function focusInput() {
input.focus();
}
window.addEventListener('focus', focusInput);
window.addEventListener('click', focusInput);
focusInput();
// WASM
const response = await fetch('./lisp.wasm');
const bytes = await response.arrayBuffer();
const results = await WebAssembly.instantiate(bytes, {
env: {
jsprint: (byteOffset) => {
let s = '';
const a = new Uint8Array(memory.buffer);
for (var i = byteOffset; a[i]; i++) {
s += String.fromCharCode(a[i]);
}
console.log(s);
}
}
});
const instance = results.instance;
const memory = instance.exports.pagememory;
instance.exports.helloworld();