Playground enhancements

This commit is contained in:
2025-07-15 19:58:58 -04:00
parent 3289c95e6a
commit bb2ae861b3
73 changed files with 834 additions and 451 deletions

View File

@@ -0,0 +1,14 @@
import test from "node:test";
import assert from "node:assert";
import { b64decode, b64encode } from "./base64.ts";
test("round trip", () => {
for (let s of ["", "a", "aa", "aaa", "aaaa", "aaaaa", "aaaaaa"]) {
let t = new TextEncoder().encode(s);
console.log(t, t + "");
let enc = b64encode(t);
assert.equal(enc.length, Math.ceil((t.length * 8) / 6));
assert.equal(b64decode(b64encode(t)) + "", t + "");
console.log("---");
}
});

45
playground/src/base64.ts Normal file
View File

@@ -0,0 +1,45 @@
// tables
const i2c = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
const c2i: Record<string, number> = {};
i2c.split("").forEach((c, i) => (c2i[c] = i));
export function b64encode(data: Uint8Array): string {
let rval = "";
let i = 0;
while (i < data.length) {
let v = data[i++];
// aaaaaa aa
rval += i2c[v >> 2];
// aabbbb bbbb
v = ((v & 3) << 8) | data[i++];
rval += i2c[v >> 4];
if (i > data.length) break;
// bbbbcc cccccc
v = ((v & 15) << 8) | data[i++];
rval += i2c[v >> 6];
if (i > data.length) break;
// cccccc
rval += i2c[v & 63];
}
return rval;
}
export function b64decode(s: string) {
let i = 0;
let arr: number[] = [];
while (i < s.length) {
// aaaaaabb bbbb
let acc = (c2i[s[i++]] << 6) | c2i[s[i++]];
arr.push(acc >> 4);
if (i >= s.length) break;
// bbbbcccc cc
acc = ((acc & 15) << 6) | c2i[s[i++]];
arr.push(acc >> 2);
if (i >= s.length) break;
// ccdddddd
acc = ((acc & 3) << 6) | c2i[s[i++]];
arr.push(acc);
if (i >= s.length) break;
}
return Uint8Array.from(arr);
}

View File

@@ -81,7 +81,6 @@ function tokenizer(stream: StringStream, state: State): string | null {
let word = stream.current();
if (keywords.includes(word)) return "keyword";
if (word[0] >= "A" && word[0] <= "Z") return "typename";
console.log('IDENT', )
return "identifier";
}
// unhandled
@@ -136,7 +135,7 @@ export class CMEditor implements AbstractEditor {
// For indent on return
indentService.of((ctx, pos) => {
let line = ctx.lineAt(pos)
if (!line) return null
if (!line || !line.from) return null
let prevLine = ctx.lineAt(line.from - 1);
if (prevLine.text.trimEnd().match(/\b(of|where|do)\s*$/)) {
let pindent = prevLine.text.match(/^\s*/)?.[0].length ?? 0

131
playground/src/deflate.ts Normal file
View File

@@ -0,0 +1,131 @@
// This is a minimal deflate implementation.
//
// It writes data that zlib can decompress, but sticks to the built-in huffman tables
// and a simple search heuristic to keep the code size down.
//
// TODO
// - initialize offsets to something other than zero (MAXINT32)
// - offsets are all wrong.
const maxMatchOffset = 1 << 14
const tableSize = 1 << 14
const lengthExtra = [0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0];
const distExtra = [0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13];
const lengthCode : number[] = []
const lengthBase : number[] = []
const distCode : number[] = [];
const distBase : number[] = [];
(function() {
// see trees.c
let code
let length = 0
for (code = 0; code < 28; code++) {
lengthBase[code] = length;
for (let n = 0; n < (1<<lengthExtra[code]); n++) {
lengthCode[length++] = code
}
}
lengthCode[length-1] = code
// Initialize the mapping dist (0..32K) -> dist code (0..29)
let dist = 0;
for (code = 0 ; code < 16; code++) {
distBase[code] = dist;
for (let n = 0; n < (1<<distExtra[code]); n++) {
distCode[dist++] = code;
}
}
dist >>= 7; // from now on, all distances are divided by 128
for ( ; code < 30; code++) {
distBase[code] = dist << 7;
for (let n = 0; n < (1<<(distExtra[code]-7)); n++) {
distCode[256 + dist] = code;
dist++
}
}
})()
export function deflate(buf: Uint8Array, raw = true) {
let bitpos = 0
let out = new Uint8Array(65536)
function write(bits: number,value:number,backwards?: boolean) {
for (let i = 0;i < bits;i++) {
const bytepos = bitpos >> 3
const mask = 1 << (bitpos&7)
if (bytepos + 10 > out.length) {
const tmp = new Uint8Array(out.length*1.5)
tmp.set(out)
out = tmp
}
let j = backwards ? i : bits - i - 1
if (value & (1 << j)) out[bytepos] |= mask
bitpos++
}
}
function emit(value: number) {
if (value < 144) write(8, 48+value)
else if (value < 256) write(9, value-144+400)
else if (value < 280) write(7, value-256)
else write(8, value-280+192)
}
if (!raw) {
write(8,0x78,true)
write(8,0x9c,true)
}
write(1,1)
write(2,2)
const src = new DataView(buf.buffer)
const hash = (u:number) => (u * 0x1e35a7bd) >>> 18 // top 14 bits
const values = new Int32Array(tableSize)
const offsets = new Int32Array(tableSize)
const sLimit = buf.length - 3
let s = 0
let nextEmit = 0
while ( s < sLimit) {
let cur_val = src.getUint32(s, true)
let cur_hash = hash(cur_val)
const cand_val = values[cur_hash]
const cand_off = offsets[cur_hash]
offsets[cur_hash] = s
values[cur_hash] = cur_val
let offset = s - cand_off
if (0 < offset && offset < maxMatchOffset && cur_val == cand_val) {
for (;nextEmit<s;nextEmit++) emit(buf[nextEmit])
s += 4
let t = cand_off + 4
let l = 0
while (s+l < buf.length && buf[s+l] === buf[t+l] && l<255) l++
const l_code = lengthCode[l+1]
emit(l_code+257)
write(lengthExtra[l_code],l + 1 - lengthBase[l_code],true)
const d_code = (offset<257) ? distCode[offset-1] : distCode[256+((offset-1)>>7)]
write(5,d_code)
write(distExtra[d_code],offset-1-distBase[d_code],true)
s += l
nextEmit = s
} else {
s++
}
}
for (;nextEmit<buf.length;nextEmit++) emit(buf[nextEmit])
emit(256)
let adler = 1
let s2 = 0
for (let i=0;i<buf.length;i++) {
adler = (adler + buf[i] % 65521)
s2 = (s2 + adler) % 65521
}
adler |= (s2 << 16)
let len = 1 + (bitpos >> 3)
if (raw) return out.slice(0,len)
new DataView(out.buffer).setInt32(len,adler,false)
return out.slice(0, len+4)
}

View File

@@ -9,7 +9,14 @@ export interface Handle {
interface Process {
argv: string[];
platform: string;
exit(_: number): void;
stdout: {
write(s: string): unknown
};
cwd(): string;
env: Record<string,string>
__lasterr: {errno: number}
}
export interface NodeShim {
stdout: string;

View File

@@ -62,7 +62,7 @@ window.addEventListener("message", (ev: MessageEvent<Message>) => {
let { src } = ev.data;
try {
sendMessage({ type: "setConsole", messages: [] });
eval(src);
(new Function(src))();
} catch (e) {
console.log(e);
}

View File

@@ -0,0 +1,16 @@
import test from "node:test";
import assert from "node:assert";
import { readFileSync } from "node:fs";
import { deflate } from "./deflate.ts";
import { inflate } from "./inflate.ts";
import { b64encode } from "./base64.ts";
test('round trip', ()=>{
let src = readFileSync('src/inflate.ts','utf8')
let smol = deflate(new TextEncoder().encode(src))
let big = inflate(smol)
assert.equal(src, new TextDecoder().decode(big))
console.log(src.length, smol.length, b64encode(smol).length)
})

304
playground/src/inflate.ts Normal file
View File

@@ -0,0 +1,304 @@
// I wrote this inflate years ago, seems to work for zip
class BitReader {
pos = 0;
bits = 0;
acc = 0;
len: number;
data: Uint8Array;
constructor(data: Uint8Array) {
this.data = data;
this.len = data.length;
}
read(bits: number) {
while (this.bits < bits) {
if (this.pos >= this.len) throw "EOF";
this.acc |= this.data[this.pos++] << this.bits;
this.bits += 8;
}
let rval = this.acc & ((1 << bits) - 1);
this.acc >>= bits;
this.bits -= bits;
return rval;
}
read8() {
if (this.pos > this.len) throw "EOF";
// flush
if (this.bits > 0) {
this.bits = 0;
this.acc = 0;
}
return this.data[this.pos++];
}
read16() {
let rval = this.read8() * 256 + this.read8();
return rval;
}
}
class HuffDic {
limit: number[];
codes: number[];
base: number[];
constructor(lengths: number[]) {
let counts = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
let min = 0;
let max = 0;
for (let i = 0; i < lengths.length; i++) {
let len = lengths[i];
if (len != 0) {
if (len < min || min == 0) min = len;
if (len > max) max = len;
counts[len]++;
}
}
this.base = [];
this.limit = [
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
];
let code = 0;
let seq = 0;
let next_code = [];
for (let i = min; i <= max; i++) {
let n = counts[i];
next_code[i] = code;
this.base[i] = code - seq;
code += n;
seq += n;
this.limit[i] = code - 1;
code <<= 1;
}
this.codes = [];
for (let i = 0; i < lengths.length; i++) {
let n = lengths[i];
if (n != 0) {
code = next_code[n];
next_code[n]++;
if (!this.base[n]) this.base[n] = 0;
seq = code - this.base[n];
this.codes[seq] = i;
}
}
}
readSymbol(r: BitReader) {
let v = 0;
for (let i = 1; i < this.limit.length; i++) {
v <<= 1;
v |= r.read(1);
let limit = this.limit[i];
if (v <= limit) {
return this.codes[v - this.base[i]];
}
}
throw "eHUFF";
}
}
let staticHuff: HuffDic;
let distHuff: HuffDic;
{
let tmp = [];
for (let i = 0; i < 144; i++) tmp[i] = 8;
for (let i = 144; i < 256; i++) tmp[i] = 9;
for (let i = 256; i < 280; i++) tmp[i] = 7;
for (let i = 280; i < 288; i++) tmp[i] = 8;
staticHuff = new HuffDic(tmp);
tmp = [];
for (let i = 0; i < 30; i++) {
tmp[i] = 5;
}
distHuff = new HuffDic(tmp);
}
export function inflate(input: Uint8Array) {
let r = new BitReader(input);
let out = new Uint8Array(65536);
let pos = 0;
const push = (b: number) => {
if (pos + 10 > out.length) {
const tmp = new Uint8Array(out.length * 1.5);
tmp.set(out);
out = tmp;
}
out[pos++] = b;
};
let fin = 0;
while (!fin) {
fin = r.read(1);
let btype = r.read(2);
let huff2;
let huff3;
if (btype == 0) {
let len = r.read16();
let nlen = r.read16();
for (let i = 0; i < len; i++) push(r.read8());
} else if (btype == 1) {
// fixed huffman
huff2 = staticHuff;
huff3 = distHuff;
} else if (btype == 2) {
// dynamic huffman
let hlit = r.read(5) + 257;
let hdist = r.read(5) + 1;
let hclen = r.read(4) + 4;
let lengths: number[] = [];
for (let i = 0; i < 19; i++) lengths[i] = 0;
let xx = [
16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15,
];
for (let i = 0; i < hclen; i++) {
let t = r.read(3);
lengths[xx[i]] = t;
}
let huff = new HuffDic(lengths);
lengths = [];
while (true) {
let k = huff.readSymbol(r);
if (k < 16) {
lengths.push(k);
} else if (k == 16) {
let count = r.read(2) + 3;
if (lengths.length == 0) throw new Error("no lengths?");
for (; count > 0; count--) lengths.push(lengths[lengths.length - 1]);
} else if (k == 17) {
let count = r.read(3) + 3;
for (; count > 0; count--) lengths.push(0);
} else if (k == 18) {
let count = r.read(7) + 11;
for (; count > 0; count--) lengths.push(0);
}
if (lengths.length >= hlit + hdist) break;
}
huff2 = new HuffDic(lengths.slice(0, hlit));
huff3 = new HuffDic(lengths.slice(hlit));
} else {
throw new Error("btype " + btype);
}
if (huff2) {
while (true) {
let k = huff2.readSymbol(r);
let len = 0;
let n = 0; // extra bits
if (k < 256) {
push(k);
continue;
} else if (k == 256) {
// End of block
break;
} else if (k < 265) {
len = k - 257 + 3;
n = 0;
} else if (k < 269) {
len = (k - 265) * 2 + 11;
n = 1;
} else if (k < 273) {
len = (k - 269) * 4 + 19;
n = 2;
} else if (k < 277) {
len = (k - 273) * 8 + 35;
n = 3;
} else if (k < 281) {
len = (k - 277) * 16 + 67;
n = 4;
} else if (k < 285) {
len = (k - 281) * 32 + 131;
n = 5;
} else {
len = 258;
n = 0;
}
if (n > 0) len += r.read(n);
// distance
if (r.pos > r.len) throw new Error("EOF");
let dist;
if (huff3) dist = huff3.readSymbol(r);
else dist = r.read(5);
if (dist < 4) {
dist++;
} else if (dist < 30) {
let db = (dist - 2) >> 1;
let extra = (dist & 1) << db;
extra |= r.read(db);
dist = (1 << (db + 1)) + 1 + extra;
} else {
throw new Error(`dist ${dist}`);
}
if (dist > pos) throw new Error(`dist ${dist} > pos ${pos}`);
let s: number = pos - dist;
for (let i = 0; i < len; i++) push(out[s + i]);
}
}
}
return out.slice(0, pos);
}
interface Entry {
size: number;
start: number;
end: number;
}
export class ZipFile {
data: Uint8Array;
entries: Record<string, Entry>;
constructor(data: Uint8Array) {
this.data = data;
this.entries = {};
let td = new TextDecoder();
let error = (msg: string) => {
throw new Error(`${msg} at ${pos}`);
};
let view = new DataView(data.buffer);
let pos = 0;
while (pos < view.byteLength) {
let sig = view.getUint32(pos, true);
if (sig == 0x02014b50) break;
if (sig != 0x04034b50) error(`bad sig ${sig.toString(16)}`);
let method = view.getUint16(pos + 8, true);
if (method !== 8) throw new Error(`method ${method} not handled`)
let csize = view.getUint32(pos + 18, true);
let size = view.getUint32(pos + 22, true);
let fnlen = view.getUint16(pos + 26, true);
let eflen = view.getUint16(pos + 28, true);
let fn = td.decode(data.slice(pos + 30, pos + 30 + fnlen));
if (size) {
let start = pos + 30 + fnlen + eflen;
let end = start + csize;
this.entries[fn] = { size, start, end };
}
pos = pos + 30 + fnlen + eflen + csize;
}
}
getData(name: string) {
let { start, end, size } = this.entries[name];
return inflate(new Uint8Array(this.data.slice(start, end)));
}
}

74
playground/src/ipc.ts Normal file
View File

@@ -0,0 +1,74 @@
export type Result<A> =
| { status: "ok"; value: A }
| { status: "err"; error: string };
export interface API {
save(fileName: string, content: string): string;
typeCheck(fileName: string): string;
compile(fileName: string): string;
}
export interface Message<K extends keyof API> {
id: number;
key: K;
args: Parameters<API[K]>;
}
export interface ResponseMSG {
id: number;
result: string;
}
type Suspense = {
resolve: (value: any | PromiseLike<any>) => void;
reject: (reason?: any) => void;
};
export class IPC {
callbacks: Record<number, Suspense> = {};
_postMessage: <K extends keyof API>(msg: Message<K>) => void;
lastID = 1;
constructor() {
const newtWorker = new Worker("worker.js");
this._postMessage = <K extends keyof API>(msg: Message<K>) =>
newtWorker.postMessage(msg);
// Safari/MobileSafari have small stacks in webworkers.
if (navigator.vendor.includes("Apple")) {
const workerFrame = document.createElement("iframe");
workerFrame.src = "worker.html";
workerFrame.style.display = "none";
document.body.appendChild(workerFrame);
this._postMessage = (msg: any) =>
workerFrame.contentWindow?.postMessage(msg, "*");
window.addEventListener("message", (ev) => this.onmessage(ev));
} else {
newtWorker.onmessage = this.onmessage
}
// Need to handle messages from the other iframe too? Or at least ignore them.
}
onmessage = (ev: MessageEvent<ResponseMSG>) => {
console.log("GET", ev.data);
// Maybe key off of type
if (ev.data.id) {
let suspense = this.callbacks[ev.data.id];
if (suspense) {
suspense.resolve(ev.data.result);
delete this.callbacks[ev.data.id];
}
console.log("result", ev.data, "suspense", suspense);
}
}
async sendMessage<K extends keyof API>(
key: K,
args: Parameters<API[K]>
): Promise<ReturnType<API[K]>> {
return new Promise((resolve, reject) => {
let id = this.lastID++;
this.callbacks[id] = { resolve, reject };
console.log("POST", { id, key, args });
this._postMessage({ id, key, args });
});
}
}
class IPCClient {}

View File

@@ -1,32 +1,59 @@
import { effect, signal } from "@preact/signals";
import { Diagnostic } from "@codemirror/lint";
import { useEffect, useRef, useState } from "preact/hooks";
import { h, render } from "preact";
import { h, render, VNode } from "preact";
import { ChangeEvent } from "preact/compat";
import { archive, preload } from "./preload.ts";
import { b64decode, b64encode } from "./base64";
import {
AbstractEditor,
EditorDelegate,
CompileReq,
CompileRes,
Message,
TopData,
Marker,
} from "./types.ts";
import { CMEditor } from "./cmeditor.ts";
import { deflate } from "./deflate.ts";
import { inflate } from "./inflate.ts";
import { IPC } from "./ipc.ts";
let topData: undefined | TopData;
const newtWorker = new Worker("worker.js");
// :FIXME use because Safari
let postMessage = (msg: CompileReq) => newtWorker.postMessage(msg);
// Safari/MobileSafari have small stacks in webworkers.
if (navigator.vendor.includes("Apple")) {
const workerFrame = document.createElement("iframe");
workerFrame.src = "worker.html";
workerFrame.style.display = "none";
document.body.appendChild(workerFrame);
postMessage = (msg: any) => workerFrame.contentWindow?.postMessage(msg, "*");
const ipc = new IPC();
function mdline2nodes(s: string) {
let cs: (VNode|string)[] = []
let toks = s.matchAll(/(\*\*.*?\*\*)|(\*.*?\*)|(_.*?_)|[^*]+|\*/g)
for (let tok of toks) {
if (tok[1]) cs.push(h('b',{},tok[0].slice(2,-2)))
else if (tok[2]) cs.push(h('em',{},tok[0].slice(1,-1)))
else if (tok[3]) cs.push(h('em',{},tok[0].slice(1,-1)))
else cs.push(tok[0])
}
return cs
}
function md2nodes(md: string) {
let rval: VNode[] = []
let list: VNode[] | undefined
for (let line of md.split("\n")) {
if (line.startsWith('- ')) {
list = list ?? []
list.push(h('li', {}, mdline2nodes(line.slice(2))))
continue
}
if (list) {
rval.push(h('ul', {}, list))
list = undefined
}
if (line.startsWith('# ')) {
rval.push(h('h2', {}, mdline2nodes(line.slice(2))))
} else if (line.startsWith('## ')) {
rval.push(h('h3', {}, mdline2nodes(line.slice(3))))
} else {
rval.push(h('div', {}, mdline2nodes(line)))
}
}
return rval
}
// iframe for running newt output
@@ -35,13 +62,20 @@ iframe.src = "frame.html";
iframe.style.display = "none";
document.body.appendChild(iframe);
function run(src: string) {
console.log("SEND TO", iframe.contentWindow);
const fileName = state.currentFile.value;
// postMessage({ type: "compileRequest", fileName, src });
async function refreshJS() {
if (!state.javascript.value) {
let src = state.editor.value!.getValue();
console.log("SEND TO", iframe.contentWindow);
const fileName = state.currentFile.value;
// maybe send fileName, src?
await ipc.sendMessage("save", [fileName, src]);
let js = await ipc.sendMessage("compile", [fileName]);
state.javascript.value = js;
}
}
function runOutput() {
async function runOutput() {
await refreshJS()
const src = state.javascript.value;
console.log("RUN", iframe.contentWindow);
try {
@@ -68,55 +102,47 @@ function setOutput(output: string) {
state.output.value = output;
}
let lastID = 0;
const nextID = () => "" + lastID++;
window.onmessage = (ev: MessageEvent<Message>) => {
window.addEventListener("message", (ev) => {
if (ev.data.id) return;
console.log("window got", ev.data);
if ("messages" in ev.data) state.messages.value = ev.data.messages;
if ("message" in ev.data) {
state.messages.value = [...state.messages.value, ev.data.message];
}
// safari callback
if ("output" in ev.data) {
newtWorker.onmessage?.(ev)
setOutput(ev.data.output);
state.javascript.value = ev.data.javascript;
}
};
});
// TODO wrap up IPC
async function copyToClipboard(ev: Event) {
ev.preventDefault();
let src = state.editor.value!.getValue();
let hash = `#code/${b64encode(deflate(new TextEncoder().encode(src)))}`;
window.location.hash = hash;
await navigator.clipboard.writeText(window.location.href);
state.toast.value = "URL copied to clipboard";
setTimeout(() => (state.toast.value = ""), 2_000);
}
type Suspense<T> = {
resolve: (value: T | PromiseLike<T>) => void;
reject: (reason?: any) => void;
};
// We could push this into the editor
document.addEventListener("keydown", (ev) => {
if ((ev.metaKey || ev.ctrlKey) && ev.code == "KeyS")
copyToClipboard(ev);
});
const callbacks: Record<string, Suspense<string>> = {};
newtWorker.onmessage = (ev: MessageEvent<CompileRes>) => {
let suspense = callbacks[ev.data.id];
if (suspense) {
suspense.resolve(ev.data.output);
delete callbacks[ev.data.id];
}
console.log('result', ev.data, 'suspense', suspense)
// FIXME - we want to have the callback take a response for its command
setOutput(ev.data.output);
state.javascript.value = ev.data.javascript;
};
function runCommand(req: CompileReq) {
return new Promise<string>(
(resolve, reject) => {
callbacks[req.id] = { resolve, reject }
postMessage(req);
function getSavedCode() {
let value: string = localStorage.idrisCode || LOADING;
let hash = window.location.hash;
if (hash.startsWith("#code/")) {
try {
value = new TextDecoder().decode(inflate(b64decode(hash.slice(6))));
} catch (e) {
console.error(e);
}
);
}
return value;
}
const state = {
output: signal(""),
toast: signal(""),
javascript: signal(""),
messages: signal<string[]>([]),
editor: signal<AbstractEditor | null>(null),
@@ -166,7 +192,7 @@ document.addEventListener("keydown", (ev) => {
const LOADING = "module Loading\n";
let value = localStorage.code || LOADING;
let value = getSavedCode() || LOADING;
let initialVertical = localStorage.vertical == "true";
interface EditorProps {
@@ -176,13 +202,8 @@ const language: EditorDelegate = {
getEntry(word, _row, _col) {
return topData?.context.find((entry) => entry.name === word);
},
onChange(value) {
// run via the linter now
// clearTimeout(timeout);
// timeout = setTimeout(() => {
// run(value);
// localStorage.code = value;
// }, 1000);
onChange(_value) {
// we're using lint() now
},
getFileName() {
if (!topData) return "";
@@ -192,39 +213,35 @@ const language: EditorDelegate = {
async lint(view) {
console.log("LINT");
let src = view.state.doc.toString();
localStorage.code = src
let module = src.match(/module\s+([^\s]+)/)?.[1]
localStorage.code = src;
let module = src.match(/module\s+([^\s]+)/)?.[1];
if (module) {
// This causes problems with stuff like aoc/... that reference files in the same directory
// state.currentFile.value = module.replace('.','/')+'.newt'
// This causes problems with stuff like aoc/...
state.currentFile.value = module.replace(".", "/") + ".newt";
}
state.javascript.value = ''
let fileName = state.currentFile.value;
console.log("FN", fileName);
try {
let out = await runCommand({
id: nextID(),
type: "compileRequest",
fileName,
src,
compile: false,
});
console.log("OUT", out);
await ipc.sendMessage("save", [fileName, src]);
let out = await ipc.sendMessage("typeCheck", [fileName]);
setOutput(out);
let markers = processOutput(out);
let diags: Diagnostic[] = []
let diags: Diagnostic[] = [];
for (let marker of markers) {
let col = marker.startColumn
let col = marker.startColumn;
let line = view.state.doc.line(marker.startLineNumber)
let line = view.state.doc.line(marker.startLineNumber);
const pos = line.from + col - 1;
let word = view.state.wordAt(pos)
let word = view.state.wordAt(pos);
diags.push({
from: word?.from ?? pos,
to: word?.to ?? pos+1,
to: word?.to ?? pos + 1,
severity: marker.severity,
message: marker.message,
})
});
}
return diags
return diags;
} catch (e) {
console.log("ERR", e);
}
@@ -243,7 +260,6 @@ function Editor({ initialValue }: EditorProps) {
state.editor.value = editor;
editor.setDark(state.dark.value);
if (initialValue === LOADING) loadFile("Tour.newt");
else run(initialValue);
}, []);
return h("div", { id: "editor", ref });
@@ -260,6 +276,38 @@ function Result() {
return h("div", { id: "result" }, text);
}
function Help() {
return h("div", { id: "help" },
md2nodes(`
# Newt Playground
The editor will typecheck the file with newt and render errors as the file is changed. The current file is saved to localStorage and will be restored if there is no data in the URL. Cmd-s / Ctrl-s will create a url embedding the file contents. There is a layout toggle for phone use.
## Tabs
**Output** - Displays the compiler output, which is also used to render errors and info annotations in the editor.
**JS** - Displays the javascript translation of the file
**Console** - Displays the console output from running the javascript
**Help** - Displays this help file
## Buttons
▶ Compile and run the current file in an iframe, console output is collected to the console tab.
📋 Embed the current file in the URL and copy to clipboard
↕ or ↔ Toggle vertical or horziontal layout (for mobile)
## Keyboard
*C-s or M-s* - Embed the current file in the URL and copy to clipboard
`)
)
}
function Console() {
const messages = state.messages.value ?? [];
return h(
@@ -272,6 +320,7 @@ function Console() {
const RESULTS = "Output";
const JAVASCRIPT = "JS";
const CONSOLE = "Console";
const HELP = "Help";
function Tabs() {
const [selected, setSelected] = useState(localStorage.tab ?? RESULTS);
@@ -289,6 +338,10 @@ function Tabs() {
if (state.messages.value.length) setSelected(CONSOLE);
}, [state.messages.value]);
useEffect(() => {
if (selected === JAVASCRIPT && !state.javascript.value) refreshJS();
}, [selected, state.javascript.value]);
let body;
switch (selected) {
case RESULTS:
@@ -300,6 +353,9 @@ function Tabs() {
case CONSOLE:
body = h(Console, {});
break;
case HELP:
body = h(Help, {});
break;
default:
body = h("div", {});
}
@@ -312,7 +368,8 @@ function Tabs() {
{ className: "tabBar" },
Tab(RESULTS),
Tab(JAVASCRIPT),
Tab(CONSOLE)
Tab(CONSOLE),
Tab(HELP),
),
h("div", { className: "tabBody" }, body)
);
@@ -348,12 +405,15 @@ function EditWrap({
loadFile(fn);
}
};
let d = vertical
? "M0 0 h20 v20 h-20 z M0 10 h20"
: "M0 0 h20 v20 h-20 z M10 0 v20";
let play = "M0 0 L20 10 L0 20 z";
let svg = (d: string) =>
h("svg", { width: 20, height: 20, className: "icon" }, h("path", { d }));
const char = vertical ? "↕" : "↔"
// let d = vertical
// ? "M0 0 h20 v20 h-20 z M0 10 h20"
// : "M0 0 h20 v20 h-20 z M10 0 v20";
// let play = "M0 0 L20 10 L0 20 z";
// let svg = (d: string) =>
// h("svg", { width: 20, height: 20, className: "icon" }, h("path", { d }));
return h(
"div",
{ className: "tabPanel left" },
@@ -366,8 +426,10 @@ function EditWrap({
options
),
h("div", { style: { flex: "1 1" } }),
h("button", { onClick: runOutput }, svg(play)),
h("button", { onClick: toggle }, svg(d))
h("button", { onClick: copyToClipboard, title: "copy url" }, "📋"),
h("button", { onClick: runOutput, title: "run program" }, "▶"),
h("button", { onClick: toggle, title: "change layout" }, char),
// h("button", { onClick: toggle }, svg(d))
),
h("div", { className: "tabBody" }, h(Editor, { initialValue: value }))
);
@@ -379,10 +441,15 @@ function App() {
setVertical(!vertical);
localStorage.vertical = !vertical;
};
let toast;
if (state.toast.value) {
toast = h("p", { className: "toast" }, h("div", {}, state.toast.value));
}
let className = `wrapper ${vertical ? "vertical" : "horizontal"}`;
return h(
"div",
{ className },
toast,
h(EditWrap, { vertical, toggle }),
h(Tabs, {})
);
@@ -390,16 +457,10 @@ function App() {
render(h(App, {}), document.getElementById("app")!);
let timeout: number | undefined;
// Adapted from the vscode extension, but types are slightly different
// and positions are 1-based.
const processOutput = (
// editor: AbstractEditor,
output: string
) => {
// let model = editor.getModel()!;
console.log('process output', output)
console.log("process output", output);
let markers: Marker[] = [];
let lines = output.split("\n");
let m = lines[0].match(/.*Process (.*)/);
@@ -407,7 +468,9 @@ const processOutput = (
if (m) fn = m[1];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const match = line.match(/(INFO|ERROR) at ([^:]+):\((\d+), (\d+)\):\s*(.*)/);
const match = line.match(
/(INFO|ERROR) at ([^:]+):\((\d+), (\d+)\):\s*(.*)/
);
if (match) {
let [_full, kind, file, line, col, message] = match;
let lineNumber = +line + 1;
@@ -428,7 +491,7 @@ const processOutput = (
}
if (kind === "ERROR" || lineNumber)
markers.push({
severity: kind === 'ERROR' ? 'error' : 'info',
severity: kind === "ERROR" ? "error" : "info",
message,
startLineNumber: lineNumber,
endLineNumber: lineNumber,
@@ -437,7 +500,7 @@ const processOutput = (
});
}
}
console.log('markers', markers)
console.log("markers", markers);
// editor.setMarkers(markers)
return markers;
};

View File

@@ -1,6 +1,8 @@
import { EditorView } from "codemirror";
import { linter, Diagnostic } from "@codemirror/lint";
export interface CompileReq {
id: string
type: "compileRequest";

View File

@@ -1,41 +1,50 @@
import { shim } from "./emul";
import { API, Message, ResponseMSG } from "./ipc";
import { archive, preload } from "./preload";
import { CompileReq, CompileRes } from "./types";
const LOG = console.log
console.log = (m) => {
shim.stdout += "\n" + m;
};
const handleMessage = async function (ev: { data: CompileReq }) {
console.log("message", ev.data);
const handleMessage = async function <K extends keyof API>(ev: { data: Message<K> }) {
LOG("HANDLE", ev.data);
await preload;
shim.archive = archive;
let { id, src, fileName } = ev.data;
const outfile = "out.js";
if (ev.data.compile)
shim.process.argv = ["browser", "newt", fileName, "-o", outfile, "--top"];
else
shim.process.argv = ["browser", "newt", fileName, "--top"];
shim.files[fileName] = new TextEncoder().encode(src);
shim.files[outfile] = new TextEncoder().encode("No JS output");
shim.stdout = "";
const start = +new Date();
try {
Main_main();
} catch (e) {
// make it clickable in console
console.error(e);
// make it visable on page
shim.stdout += "\n" + String(e);
let key = ev.data.key
if (key === 'typeCheck' || key === 'compile') {
let {id, args: [fileName]} = ev.data
LOG(key, fileName)
const outfile = "out.js";
const isCompile = key === 'compile';
if (isCompile)
shim.process.argv = ["browser", "newt", fileName, "-o", outfile, "--top"];
else
shim.process.argv = ["browser", "newt", fileName, "--top"];
shim.stdout = "";
shim.files[outfile] = new TextEncoder().encode("No JS output");
try {
Main_main();
} catch (e) {
// make it clickable in console
console.error(e);
// make it visable on page
shim.stdout += "\n" + String(e);
}
let result = isCompile ? new TextDecoder().decode(shim.files[outfile]) : shim.stdout
sendResponse({id, result})
} else if (key === 'save') {
let {id, args: [fileName, content]} = ev.data
LOG(`SAVE ${content?.length} to ${fileName}`)
shim.files[fileName] = new TextEncoder().encode(content)
LOG('send', {id, result: ''})
sendResponse({id, result: ''})
}
let duration = +new Date() - start;
console.log(`process ${fileName} in ${duration} ms`);
let javascript = new TextDecoder().decode(shim.files[outfile]);
let output = shim.stdout;
sendResponse({ id, type: "compileResult", javascript, output, duration });
};
// hooks for worker.html to override
let sendResponse: (_: CompileRes) => void = postMessage;
let sendResponse: <K extends keyof API>(_: ResponseMSG) => void = postMessage;
onmessage = handleMessage;
importScripts("newt.js");

View File

@@ -1,269 +1,11 @@
import { inflate } from "./inflate";
// I wrote this inflate years ago, seems to work for zip
class BitReader {
pos = 0;
bits = 0;
acc = 0;
len: number;
data: Uint8Array;
constructor(data: Uint8Array) {
this.data = data;
this.len = data.length;
}
read(bits: number) {
while (this.bits < bits) {
if (this.pos >= this.len) throw "EOF";
this.acc |= this.data[this.pos++] << this.bits;
this.bits += 8;
}
let rval = this.acc & ((1 << bits) - 1);
this.acc >>= bits;
this.bits -= bits;
return rval;
}
read8() {
if (this.pos > this.len) throw "EOF";
// flush
if (this.bits > 0) {
this.bits = 0;
this.acc = 0;
}
return this.data[this.pos++];
}
read16() {
let rval = this.read8() * 256 + this.read8();
return rval;
}
}
class HuffDic {
limit: number[];
codes: number[];
base: number[];
constructor(lengths: number[]) {
let counts = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
let min = 0;
let max = 0;
for (let i = 0; i < lengths.length; i++) {
let len = lengths[i];
if (len != 0) {
if (len < min || min == 0) min = len;
if (len > max) max = len;
counts[len]++;
}
}
this.base = [];
this.limit = [
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
];
let code = 0;
let seq = 0;
let next_code = [];
for (let i = min; i <= max; i++) {
let n = counts[i];
next_code[i] = code;
this.base[i] = code - seq;
code += n;
seq += n;
this.limit[i] = code - 1;
code <<= 1;
}
this.codes = [];
for (let i = 0; i < lengths.length; i++) {
let n = lengths[i];
if (n != 0) {
code = next_code[n];
next_code[n]++;
if (!this.base[n]) this.base[n] = 0;
seq = code - this.base[n];
this.codes[seq] = i;
}
}
}
readSymbol(r: BitReader) {
let v = 0;
let l = 0;
let offset = 0;
for (let i = 1; i < this.limit.length; i++) {
v <<= 1;
v |= r.read(1);
let limit = this.limit[i];
if (v <= limit) {
return this.codes[v - this.base[i]];
}
}
throw "eHUFF";
}
}
let staticHuff: HuffDic;
let distHuff: HuffDic;
{
let tmp = [];
for (let i = 0; i < 144; i++) tmp[i] = 8;
for (let i = 144; i < 256; i++) tmp[i] = 9;
for (let i = 256; i < 280; i++) tmp[i] = 7;
for (let i = 280; i < 288; i++) tmp[i] = 8;
staticHuff = new HuffDic(tmp);
tmp = [];
for (let i = 0; i < 30; i++) {
tmp[i] = 5;
}
distHuff = new HuffDic(tmp);
}
function inflate(input: Uint8Array) {
let r = new BitReader(input);
let out = new Uint8Array(65536);
let pos = 0;
const push = (b: number) => {
if (pos + 10 > out.length) {
const tmp = new Uint8Array(out.length * 1.5);
tmp.set(out);
out = tmp;
}
out[pos++] = b;
};
let fin = 0;
while (!fin) {
fin = r.read(1);
let btype = r.read(2);
let huff2;
let huff3;
if (btype == 0) {
let len = r.read16();
let nlen = r.read16();
for (let i = 0; i < len; i++) push(r.read8());
} else if (btype == 1) {
// fixed huffman
huff2 = staticHuff;
huff3 = distHuff;
} else if (btype == 2) {
// dynamic huffman
let hlit = r.read(5) + 257;
let hdist = r.read(5) + 1;
let hclen = r.read(4) + 4;
let lengths: number[] = [];
for (let i = 0; i < 19; i++) lengths[i] = 0;
let xx = [
16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15,
];
for (let i = 0; i < hclen; i++) {
let t = r.read(3);
lengths[xx[i]] = t;
}
let huff = new HuffDic(lengths);
lengths = [];
while (true) {
let k = huff.readSymbol(r);
if (k < 16) {
lengths.push(k);
} else if (k == 16) {
let count = r.read(2) + 3;
if (lengths.length == 0) throw new Error("no lengths?");
for (; count > 0; count--) lengths.push(lengths[lengths.length - 1]);
} else if (k == 17) {
let count = r.read(3) + 3;
for (; count > 0; count--) lengths.push(0);
} else if (k == 18) {
let count = r.read(7) + 11;
for (; count > 0; count--) lengths.push(0);
}
if (lengths.length >= hlit + hdist) break;
}
huff2 = new HuffDic(lengths.slice(0, hlit));
huff3 = new HuffDic(lengths.slice(hlit));
} else {
throw new Error("btype " + btype);
}
if (huff2) {
while (true) {
let k = huff2.readSymbol(r);
let len = 0;
let n = 0; // extra bits
if (k < 256) {
push(k);
continue;
} else if (k == 256) {
// End of block
break;
} else if (k < 265) {
len = k - 257 + 3;
n = 0;
} else if (k < 269) {
len = (k - 265) * 2 + 11;
n = 1;
} else if (k < 273) {
len = (k - 269) * 4 + 19;
n = 2;
} else if (k < 277) {
len = (k - 273) * 8 + 35;
n = 3;
} else if (k < 281) {
len = (k - 277) * 16 + 67;
n = 4;
} else if (k < 285) {
len = (k - 281) * 32 + 131;
n = 5;
} else {
len = 258;
n = 0;
}
if (n > 0) len += r.read(n);
// distance
if (r.pos > r.len) throw new Error("EOF");
let dist;
if (huff3) dist = huff3.readSymbol(r);
else dist = r.read(5);
if (dist < 4) {
dist++;
} else if (dist < 30) {
let db = (dist - 2) >> 1;
let extra = (dist & 1) << db;
extra |= r.read(db);
dist = (1 << (db + 1)) + 1 + extra;
} else {
throw new Error(`dist ${dist}`);
}
if (dist > pos) throw new Error(`dist ${dist} > pos ${pos}`);
let s: number = pos - dist;
for (let i = 0; i < len; i++) push(out[s + i]);
}
}
}
return out.slice(0, pos);
}
interface Entry {
size: number;
start: number;
end: number;
method: number;
}
export class ZipFile {
@@ -292,13 +34,12 @@ export class ZipFile {
if (size) {
let start = pos + 30 + fnlen + eflen;
let end = start + csize;
this.entries[fn] = { size, start, end };
this.entries[fn] = { size, start, end, method };
}
pos = pos + 30 + fnlen + eflen + csize;
}
}
getData(name: string) {
if (!(name in this.entries)) return
let { start, end, size } = this.entries[name];
return inflate(new Uint8Array(this.data.slice(start, end)));
}